@themoltnet/agent-daemon 0.48.0 → 0.49.1
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/dist/cli.js +564 -40
- package/package.json +9 -8
package/dist/cli.js
CHANGED
|
@@ -12,7 +12,7 @@ import { AgentRuntime, ApiTaskReporter, ApiTaskSource, PollingApiTaskSource, cre
|
|
|
12
12
|
import { GuestEnvironmentBoundaryError, assertGuestEnvironmentBoundary, createPiRetryTriage, findMainWorktree, isResolvedPathInsideRoot, normalizeRetryTriageResult, redactRetryTriageSecrets, resolveRuntimeProfileModel } from "@themoltnet/pi-runtime";
|
|
13
13
|
import { FILE_SECRET_PROVIDER, FileSecretProvider, connect, createNodeSecretProviderRegistry } from "@themoltnet/sdk/node";
|
|
14
14
|
import { execFile, execFileSync, spawn } from "node:child_process";
|
|
15
|
-
import { constants, createReadStream, createWriteStream, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { constants, createReadStream, createWriteStream, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
16
16
|
import { AuthenticationError, MoltNetError, agentKeyKey, assertTrustedConfigApiUrl, createExecutorAttestor, deriveMcpUrl, formatSecretReferenceString, identitySeedKey, parseSecretReferenceString, readConfig, register, requireSecureCredentialApiUrl, resolveAgentKey, resolveEnvSecretReference, resolveIdentitySeed, resolveOAuth2ClientSecret } from "@themoltnet/sdk";
|
|
17
17
|
import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
18
18
|
import { once } from "node:events";
|
|
@@ -30,6 +30,8 @@ import { mkdir, open, realpath, stat } from "node:fs/promises";
|
|
|
30
30
|
import { pipeline } from "node:stream/promises";
|
|
31
31
|
import cors from "@fastify/cors";
|
|
32
32
|
import helmet from "@fastify/helmet";
|
|
33
|
+
import { getOAuthProviders } from "@earendil-works/pi-ai/oauth";
|
|
34
|
+
import { AuthStorage } from "@earendil-works/pi-coding-agent";
|
|
33
35
|
import { Transform, Writable } from "node:stream";
|
|
34
36
|
import { writePiConfig } from "@themoltnet/pi-runtime/pi-config";
|
|
35
37
|
import { homedir } from "node:os";
|
|
@@ -4424,13 +4426,11 @@ function attestPreparedRuntime(prepared, signingPrivateKey) {
|
|
|
4424
4426
|
//#endregion
|
|
4425
4427
|
//#region src/lib/retry-triage.ts
|
|
4426
4428
|
var RETRYABLE_CODES = new Set([
|
|
4427
|
-
"checkpoint_upload_failed",
|
|
4428
4429
|
"complete_call_failed",
|
|
4429
4430
|
"daemon_abort",
|
|
4430
4431
|
"dispatch_expired",
|
|
4431
4432
|
"lease_expired",
|
|
4432
4433
|
"llm_api_error",
|
|
4433
|
-
"runtime_session_checkpoint_failed",
|
|
4434
4434
|
"session_prompt_failed"
|
|
4435
4435
|
]);
|
|
4436
4436
|
var NON_RETRYABLE_CODES = new Set([
|
|
@@ -4480,7 +4480,7 @@ var NON_RETRYABLE_MESSAGE_PATTERNS = [
|
|
|
4480
4480
|
async function classifyAttemptFailure(input) {
|
|
4481
4481
|
const deterministic = classifyDeterministically(input.error);
|
|
4482
4482
|
if (input.remainingAttempts !== null && input.remainingAttempts !== void 0 && input.remainingAttempts <= 0) {
|
|
4483
|
-
const deterministicReason = deterministic === "ambiguous" ? "" :
|
|
4483
|
+
const deterministicReason = deterministic === "ambiguous" ? "" : deterministic === "retryable" ? " The failure type is retryable, but no attempts remain." : " The failure type is non-retryable.";
|
|
4484
4484
|
return {
|
|
4485
4485
|
error: withRetryInfo(input.error, {
|
|
4486
4486
|
retryable: false,
|
|
@@ -4546,6 +4546,7 @@ async function classifyAttemptFailure(input) {
|
|
|
4546
4546
|
function classifyDeterministically(error) {
|
|
4547
4547
|
const code = error.code.toLowerCase();
|
|
4548
4548
|
const message = error.message;
|
|
4549
|
+
if (code === "runtime_session_upload_failed") return error.retryable === true ? "retryable" : "non_retryable";
|
|
4549
4550
|
if (NON_RETRYABLE_CODES.has(code)) return "non_retryable";
|
|
4550
4551
|
if (NON_RETRYABLE_MESSAGE_PATTERNS.some((pattern) => pattern.test(message))) return "non_retryable";
|
|
4551
4552
|
if (RETRYABLE_CODES.has(code)) return "retryable";
|
|
@@ -5539,15 +5540,29 @@ function applyRuntimeSessionUploadFailure(output, err) {
|
|
|
5539
5540
|
error: {
|
|
5540
5541
|
code: "runtime_session_upload_failed",
|
|
5541
5542
|
message: "Task completed, but durable runtime session checkpoint upload failed: " + (err instanceof Error ? err.message : String(err)),
|
|
5542
|
-
retryable:
|
|
5543
|
+
retryable: isTransientUploadError(err)
|
|
5543
5544
|
},
|
|
5544
5545
|
output: null,
|
|
5545
5546
|
outputCid: null,
|
|
5546
5547
|
status: "failed"
|
|
5547
5548
|
};
|
|
5548
5549
|
}
|
|
5550
|
+
/**
|
|
5551
|
+
* Transient faults worth retrying in-attempt: network-level errors
|
|
5552
|
+
* (no HTTP status at all) and 5xx/429 responses. A 4xx (auth,
|
|
5553
|
+
* validation, not-found) will not heal on retry.
|
|
5554
|
+
*/
|
|
5555
|
+
function isTransientUploadError(error) {
|
|
5556
|
+
if (!(error instanceof Error)) return false;
|
|
5557
|
+
const statusCode = error.statusCode;
|
|
5558
|
+
if (typeof statusCode !== "number") return true;
|
|
5559
|
+
return statusCode >= 500 || statusCode === 429;
|
|
5560
|
+
}
|
|
5561
|
+
var defaultSleep = (ms) => new Promise((resolve) => {
|
|
5562
|
+
setTimeout(resolve, ms);
|
|
5563
|
+
});
|
|
5549
5564
|
function createApiRuntimeSessionStore(args) {
|
|
5550
|
-
const { agent } = args;
|
|
5565
|
+
const { agent, logger, uploadRetry } = args;
|
|
5551
5566
|
return {
|
|
5552
5567
|
async findRuntimeSessionByTaskAttempt(teamId, taskId, attemptN) {
|
|
5553
5568
|
return agent.runtimeSessions.getForAttempt({
|
|
@@ -5568,18 +5583,40 @@ function createApiRuntimeSessionStore(args) {
|
|
|
5568
5583
|
async uploadAttemptFinal(input) {
|
|
5569
5584
|
const sessionPath = resolveLatestPiSessionPath(input.sessionDir);
|
|
5570
5585
|
if (!sessionPath) throw new Error(`Cannot upload runtime session for ${input.taskId}/${input.attemptN}: no local session file in ${input.sessionDir}`);
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5586
|
+
const maxTries = uploadRetry?.maxTries ?? 3;
|
|
5587
|
+
const baseDelayMs = uploadRetry?.baseDelayMs ?? 750;
|
|
5588
|
+
const sleep = uploadRetry?.sleep ?? defaultSleep;
|
|
5589
|
+
for (let tryN = 1;; tryN += 1) try {
|
|
5590
|
+
await agent.runtimeSessions.upload({
|
|
5591
|
+
attemptN: input.attemptN,
|
|
5592
|
+
taskId: input.taskId
|
|
5593
|
+
}, createReadStream(sessionPath), {
|
|
5594
|
+
parentSessionId: input.parentSessionId ?? void 0,
|
|
5595
|
+
sessionKind: input.sessionKind,
|
|
5596
|
+
sourceRuntimeProfileId: input.sourceRuntimeProfileId ?? void 0,
|
|
5597
|
+
sourceSlotId: input.sourceSlotId ?? void 0
|
|
5598
|
+
}, { teamId: input.teamId });
|
|
5599
|
+
return;
|
|
5600
|
+
} catch (error) {
|
|
5601
|
+
if (tryN >= maxTries || !isTransientUploadError(error)) throw error;
|
|
5602
|
+
const delayMs = baseDelayMs * tryN;
|
|
5603
|
+
logger?.warn({
|
|
5604
|
+
event: "agent-daemon.runtime_session_upload_retry",
|
|
5605
|
+
attemptN: input.attemptN,
|
|
5606
|
+
delayMs,
|
|
5607
|
+
statusCode: uploadStatusCode(error),
|
|
5608
|
+
taskId: input.taskId,
|
|
5609
|
+
tryN
|
|
5610
|
+
}, "Retrying durable runtime session upload");
|
|
5611
|
+
await sleep(delayMs);
|
|
5612
|
+
}
|
|
5580
5613
|
}
|
|
5581
5614
|
};
|
|
5582
5615
|
}
|
|
5616
|
+
function uploadStatusCode(error) {
|
|
5617
|
+
const statusCode = error?.statusCode;
|
|
5618
|
+
return typeof statusCode === "number" ? statusCode : void 0;
|
|
5619
|
+
}
|
|
5583
5620
|
function resolveContinueFrom(claimedTask) {
|
|
5584
5621
|
return claimedTask.task.input.continueFrom;
|
|
5585
5622
|
}
|
|
@@ -5956,7 +5993,10 @@ async function runPolling(opts) {
|
|
|
5956
5993
|
throw new Error(`No safe runtime profiles remain. ${details}`);
|
|
5957
5994
|
}
|
|
5958
5995
|
const slotRegistry = createApiRuntimeSlotStore({ agent: ctx.agent });
|
|
5959
|
-
const runtimeSessionStore = createApiRuntimeSessionStore({
|
|
5996
|
+
const runtimeSessionStore = createApiRuntimeSessionStore({
|
|
5997
|
+
agent: ctx.agent,
|
|
5998
|
+
logger: { warn: (context, message) => rootLogger.warn(context, message) }
|
|
5999
|
+
});
|
|
5960
6000
|
const sourceAttemptResolver = createApiSourceAttemptResolver({ agent: ctx.agent });
|
|
5961
6001
|
const runtimeInstanceId = createRuntimeInstanceId();
|
|
5962
6002
|
const runtimes = /* @__PURE__ */ new Map();
|
|
@@ -6607,7 +6647,10 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
|
|
|
6607
6647
|
activatePiCodingAgentDir(piAgentDir.path);
|
|
6608
6648
|
const stateDirs = ensureDaemonStateDirs(sandbox.rootDir);
|
|
6609
6649
|
const slotRegistry = createApiRuntimeSlotStore({ agent: ctx.agent });
|
|
6610
|
-
const runtimeSessionStore = createApiRuntimeSessionStore({
|
|
6650
|
+
const runtimeSessionStore = createApiRuntimeSessionStore({
|
|
6651
|
+
agent: ctx.agent,
|
|
6652
|
+
logger: { warn: (context, message) => rootLogger.warn(context, message) }
|
|
6653
|
+
});
|
|
6611
6654
|
const sourceAttemptResolver = createApiSourceAttemptResolver({ agent: ctx.agent });
|
|
6612
6655
|
const runtimeInstanceId = createRuntimeInstanceId();
|
|
6613
6656
|
const slotIdentity = {
|
|
@@ -7277,6 +7320,258 @@ function renderPairingResultPage(input) {
|
|
|
7277
7320
|
</html>
|
|
7278
7321
|
`;
|
|
7279
7322
|
}
|
|
7323
|
+
//#endregion
|
|
7324
|
+
//#region src/lib/serve/provider-login.ts
|
|
7325
|
+
/**
|
|
7326
|
+
* Subscription-provider OAuth brokering for `serve` (#2061 slice 4).
|
|
7327
|
+
*
|
|
7328
|
+
* The console clicks "Connect"; serve runs the Pi OAuth flow host-side via
|
|
7329
|
+
* `AuthStorage.login()` (which owns persistence into the shared
|
|
7330
|
+
* `pi/auth.json` and token rotation thereafter). The browser only ever sees
|
|
7331
|
+
* the provider's authorize URL or device code — never tokens.
|
|
7332
|
+
*
|
|
7333
|
+
* Provider ids come from pi-ai's own OAuth registry (`getOAuthProviders`),
|
|
7334
|
+
* so serve stays in lockstep with what Pi can actually authenticate
|
|
7335
|
+
* (anthropic, openai-codex, github-copilot, …) without hardcoding.
|
|
7336
|
+
*/
|
|
7337
|
+
var LOGIN_TTL_MS = 600 * 1e3;
|
|
7338
|
+
/** How long `start()` waits for the flow to surface a URL / device code. */
|
|
7339
|
+
var START_INFO_TIMEOUT_MS = 5e3;
|
|
7340
|
+
var ServeSubscriptionError = class extends Error {
|
|
7341
|
+
name = "ServeSubscriptionError";
|
|
7342
|
+
constructor(code, message) {
|
|
7343
|
+
super(message);
|
|
7344
|
+
this.code = code;
|
|
7345
|
+
}
|
|
7346
|
+
};
|
|
7347
|
+
var silentLogger = {
|
|
7348
|
+
info: () => void 0,
|
|
7349
|
+
warn: () => void 0,
|
|
7350
|
+
error: () => void 0
|
|
7351
|
+
};
|
|
7352
|
+
var ProviderLoginService = class {
|
|
7353
|
+
logins = /* @__PURE__ */ new Map();
|
|
7354
|
+
authStorage;
|
|
7355
|
+
logger;
|
|
7356
|
+
constructor(options) {
|
|
7357
|
+
this.options = options;
|
|
7358
|
+
this.authStorage = options.authStorage ?? AuthStorage.create(this.options.authPath);
|
|
7359
|
+
this.logger = options.logger ?? silentLogger;
|
|
7360
|
+
}
|
|
7361
|
+
now() {
|
|
7362
|
+
return this.options.now?.() ?? Date.now();
|
|
7363
|
+
}
|
|
7364
|
+
providers() {
|
|
7365
|
+
return this.options.listProviders?.() ?? getOAuthProviders().map((provider) => ({
|
|
7366
|
+
id: provider.id,
|
|
7367
|
+
name: provider.name
|
|
7368
|
+
}));
|
|
7369
|
+
}
|
|
7370
|
+
connected(providerId) {
|
|
7371
|
+
if (this.options.isConnected) return this.options.isConnected(providerId);
|
|
7372
|
+
try {
|
|
7373
|
+
return (this.options.authStorage ? this.authStorage : AuthStorage.create(this.options.authPath)).getAuthStatus(providerId).configured;
|
|
7374
|
+
} catch (error) {
|
|
7375
|
+
this.logger.warn({
|
|
7376
|
+
event: "serve.subscription_auth_read_failed",
|
|
7377
|
+
providerId,
|
|
7378
|
+
...safeLoginError(error)
|
|
7379
|
+
}, "Could not read subscription authentication state");
|
|
7380
|
+
return false;
|
|
7381
|
+
}
|
|
7382
|
+
}
|
|
7383
|
+
list() {
|
|
7384
|
+
this.sweep();
|
|
7385
|
+
return this.providers().map((provider) => ({
|
|
7386
|
+
...provider,
|
|
7387
|
+
connected: this.connected(provider.id)
|
|
7388
|
+
}));
|
|
7389
|
+
}
|
|
7390
|
+
sweep() {
|
|
7391
|
+
const now = this.now();
|
|
7392
|
+
for (const [id, login] of this.logins) if (login.startedAt + LOGIN_TTL_MS <= now) {
|
|
7393
|
+
this.invalidate(login, "expired");
|
|
7394
|
+
this.logins.delete(id);
|
|
7395
|
+
}
|
|
7396
|
+
}
|
|
7397
|
+
restoreCredential(login) {
|
|
7398
|
+
try {
|
|
7399
|
+
if (login.previousCredential) this.authStorage.set(login.providerId, login.previousCredential);
|
|
7400
|
+
else this.authStorage.logout(login.providerId);
|
|
7401
|
+
} catch (error) {
|
|
7402
|
+
this.logger.error({
|
|
7403
|
+
event: "serve.subscription_login_cleanup_failed",
|
|
7404
|
+
operationId: login.operationId,
|
|
7405
|
+
providerId: login.providerId,
|
|
7406
|
+
...safeLoginError(error)
|
|
7407
|
+
}, "Could not restore subscription credentials after an invalidated login");
|
|
7408
|
+
}
|
|
7409
|
+
}
|
|
7410
|
+
invalidate(login, transition) {
|
|
7411
|
+
if (login.invalidated) return;
|
|
7412
|
+
login.invalidated = true;
|
|
7413
|
+
login.abort.abort(/* @__PURE__ */ new Error(`subscription login ${transition}`));
|
|
7414
|
+
this.restoreCredential(login);
|
|
7415
|
+
login.infoArrived();
|
|
7416
|
+
this.logger.info({
|
|
7417
|
+
event: "serve.subscription_login_transition",
|
|
7418
|
+
operationId: login.operationId,
|
|
7419
|
+
providerId: login.providerId,
|
|
7420
|
+
transition
|
|
7421
|
+
}, "Subscription login invalidated");
|
|
7422
|
+
}
|
|
7423
|
+
status(providerId) {
|
|
7424
|
+
this.sweep();
|
|
7425
|
+
const login = this.logins.get(providerId);
|
|
7426
|
+
if (!login) throw new ServeSubscriptionError("login_not_found", `no login in progress for "${providerId}"`);
|
|
7427
|
+
return snapshot(login);
|
|
7428
|
+
}
|
|
7429
|
+
/**
|
|
7430
|
+
* Start (or return the in-flight) login for a provider. Resolves once the
|
|
7431
|
+
* flow has surfaced an authorize URL / device code, completed, or the
|
|
7432
|
+
* start window elapsed — whichever comes first.
|
|
7433
|
+
*/
|
|
7434
|
+
async start(providerId) {
|
|
7435
|
+
this.sweep();
|
|
7436
|
+
if (!this.providers().some((provider) => provider.id === providerId)) throw new ServeSubscriptionError("provider_unknown", `"${providerId}" is not a known subscription provider`);
|
|
7437
|
+
const existing = this.logins.get(providerId);
|
|
7438
|
+
if (existing && existing.status === "pending") return snapshot(existing);
|
|
7439
|
+
let infoArrived = () => void 0;
|
|
7440
|
+
const infoPromise = new Promise((resolvePromise) => {
|
|
7441
|
+
infoArrived = () => resolvePromise();
|
|
7442
|
+
});
|
|
7443
|
+
const abort = new AbortController();
|
|
7444
|
+
const login = {
|
|
7445
|
+
providerId,
|
|
7446
|
+
status: "pending",
|
|
7447
|
+
operationId: randomUUID(),
|
|
7448
|
+
startedAt: this.now(),
|
|
7449
|
+
infoArrived,
|
|
7450
|
+
abort,
|
|
7451
|
+
invalidated: false,
|
|
7452
|
+
previousCredential: this.authStorage.get(providerId)
|
|
7453
|
+
};
|
|
7454
|
+
this.logins.set(providerId, login);
|
|
7455
|
+
this.logger.info({
|
|
7456
|
+
event: "serve.subscription_login_transition",
|
|
7457
|
+
operationId: login.operationId,
|
|
7458
|
+
providerId,
|
|
7459
|
+
transition: "started"
|
|
7460
|
+
}, "Subscription login started");
|
|
7461
|
+
const callbacks = createLoginCallbacks(login, this.logger);
|
|
7462
|
+
(this.options.runLogin ?? ((id, loginCallbacks) => this.authStorage.login(id, loginCallbacks)))(providerId, callbacks).then(() => {
|
|
7463
|
+
if (login.invalidated) {
|
|
7464
|
+
this.restoreCredential(login);
|
|
7465
|
+
return;
|
|
7466
|
+
}
|
|
7467
|
+
if (!this.options.runLogin && !this.connected(providerId)) {
|
|
7468
|
+
login.status = "failed";
|
|
7469
|
+
login.error = "Subscription sign-in completed, but credentials were not persisted. Start again to retry.";
|
|
7470
|
+
this.logger.error({
|
|
7471
|
+
event: "serve.subscription_login_transition",
|
|
7472
|
+
operationId: login.operationId,
|
|
7473
|
+
providerId,
|
|
7474
|
+
transition: "persistence_failed"
|
|
7475
|
+
}, "Subscription login credentials were not persisted");
|
|
7476
|
+
} else {
|
|
7477
|
+
login.status = "completed";
|
|
7478
|
+
this.logger.info({
|
|
7479
|
+
event: "serve.subscription_login_transition",
|
|
7480
|
+
operationId: login.operationId,
|
|
7481
|
+
providerId,
|
|
7482
|
+
transition: "completed"
|
|
7483
|
+
}, "Subscription login completed");
|
|
7484
|
+
}
|
|
7485
|
+
login.infoArrived();
|
|
7486
|
+
}, (error) => {
|
|
7487
|
+
if (login.invalidated) {
|
|
7488
|
+
this.restoreCredential(login);
|
|
7489
|
+
return;
|
|
7490
|
+
}
|
|
7491
|
+
login.status = "failed";
|
|
7492
|
+
login.error = publicLoginError(error);
|
|
7493
|
+
this.logger.warn({
|
|
7494
|
+
event: "serve.subscription_login_transition",
|
|
7495
|
+
operationId: login.operationId,
|
|
7496
|
+
providerId,
|
|
7497
|
+
transition: "failed",
|
|
7498
|
+
...safeLoginError(error)
|
|
7499
|
+
}, "Subscription login failed");
|
|
7500
|
+
login.infoArrived();
|
|
7501
|
+
});
|
|
7502
|
+
await Promise.race([infoPromise, new Promise((resolvePromise) => {
|
|
7503
|
+
setTimeout(resolvePromise, START_INFO_TIMEOUT_MS).unref?.();
|
|
7504
|
+
})]);
|
|
7505
|
+
return snapshot(login);
|
|
7506
|
+
}
|
|
7507
|
+
/** Abort an in-flight login and forget it. */
|
|
7508
|
+
cancel(providerId) {
|
|
7509
|
+
const login = this.logins.get(providerId);
|
|
7510
|
+
if (!login) throw new ServeSubscriptionError("login_not_found", `no login in progress for "${providerId}"`);
|
|
7511
|
+
this.invalidate(login, "cancelled");
|
|
7512
|
+
this.logins.delete(providerId);
|
|
7513
|
+
return {
|
|
7514
|
+
providerId,
|
|
7515
|
+
status: "cancelled"
|
|
7516
|
+
};
|
|
7517
|
+
}
|
|
7518
|
+
/** Abort every pending flow during supervisor shutdown. */
|
|
7519
|
+
close() {
|
|
7520
|
+
for (const login of this.logins.values()) this.invalidate(login, "shutdown");
|
|
7521
|
+
this.logins.clear();
|
|
7522
|
+
}
|
|
7523
|
+
};
|
|
7524
|
+
function createLoginCallbacks(login, logger) {
|
|
7525
|
+
return {
|
|
7526
|
+
onAuth: (info) => {
|
|
7527
|
+
login.authUrl = info.url;
|
|
7528
|
+
if (info.instructions) login.instructions = info.instructions;
|
|
7529
|
+
logger.info({
|
|
7530
|
+
event: "serve.subscription_login_transition",
|
|
7531
|
+
operationId: login.operationId,
|
|
7532
|
+
providerId: login.providerId,
|
|
7533
|
+
transition: "authorization_ready"
|
|
7534
|
+
}, "Subscription authorization URL ready");
|
|
7535
|
+
login.infoArrived();
|
|
7536
|
+
},
|
|
7537
|
+
onDeviceCode: (info) => {
|
|
7538
|
+
login.userCode = info.userCode;
|
|
7539
|
+
login.verificationUri = info.verificationUri;
|
|
7540
|
+
logger.info({
|
|
7541
|
+
event: "serve.subscription_login_transition",
|
|
7542
|
+
operationId: login.operationId,
|
|
7543
|
+
providerId: login.providerId,
|
|
7544
|
+
transition: "device_code_ready"
|
|
7545
|
+
}, "Subscription device code ready");
|
|
7546
|
+
login.infoArrived();
|
|
7547
|
+
},
|
|
7548
|
+
onPrompt: () => Promise.reject(new ServeSubscriptionError("login_unsupported_prompt", "This provider flow needs an interactive prompt; run `pi /login` in a terminal instead")),
|
|
7549
|
+
onSelect: (prompt) => Promise.resolve((prompt.options.find((option) => /device/i.test(option.id)) ?? prompt.options[0])?.id),
|
|
7550
|
+
signal: login.abort.signal
|
|
7551
|
+
};
|
|
7552
|
+
}
|
|
7553
|
+
function publicLoginError(error) {
|
|
7554
|
+
if (error instanceof ServeSubscriptionError) return error.message;
|
|
7555
|
+
return "Subscription sign-in failed. Start again to retry.";
|
|
7556
|
+
}
|
|
7557
|
+
function safeLoginError(error) {
|
|
7558
|
+
const result = { errorType: error instanceof Error ? error.name : typeof error };
|
|
7559
|
+
const code = error?.code;
|
|
7560
|
+
if (typeof code === "string" && /^[a-z0-9_:-]{1,64}$/iu.test(code)) result["applicationCode"] = code;
|
|
7561
|
+
return result;
|
|
7562
|
+
}
|
|
7563
|
+
function snapshot(login) {
|
|
7564
|
+
const { providerId, status, authUrl, instructions, userCode, verificationUri, error } = login;
|
|
7565
|
+
return {
|
|
7566
|
+
providerId,
|
|
7567
|
+
status,
|
|
7568
|
+
...authUrl ? { authUrl } : {},
|
|
7569
|
+
...instructions ? { instructions } : {},
|
|
7570
|
+
...userCode ? { userCode } : {},
|
|
7571
|
+
...verificationUri ? { verificationUri } : {},
|
|
7572
|
+
...error ? { error } : {}
|
|
7573
|
+
};
|
|
7574
|
+
}
|
|
7280
7575
|
var NAME_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/i;
|
|
7281
7576
|
var PROVIDER_ID_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
7282
7577
|
function assertStoreName(kind, value) {
|
|
@@ -7340,11 +7635,17 @@ var ServeStore = class {
|
|
|
7340
7635
|
agentsDir;
|
|
7341
7636
|
runsDir;
|
|
7342
7637
|
secretsDir;
|
|
7638
|
+
/** Shared Pi credential dir; `auth.json` inside is pi-managed (lockfiled). */
|
|
7639
|
+
piDir;
|
|
7343
7640
|
constructor(root) {
|
|
7344
7641
|
this.root = root;
|
|
7345
7642
|
this.agentsDir = join(root, "agents");
|
|
7346
7643
|
this.runsDir = join(root, "runs");
|
|
7347
7644
|
this.secretsDir = join(root, "secrets");
|
|
7645
|
+
this.piDir = join(root, "pi");
|
|
7646
|
+
}
|
|
7647
|
+
get piAuthJsonPath() {
|
|
7648
|
+
return join(this.piDir, "auth.json");
|
|
7348
7649
|
}
|
|
7349
7650
|
/** Create the directory layout (0700) if missing. Idempotent. */
|
|
7350
7651
|
ensure() {
|
|
@@ -7369,13 +7670,13 @@ var ServeStore = class {
|
|
|
7369
7670
|
pendingRegistrations: {},
|
|
7370
7671
|
activations: {}
|
|
7371
7672
|
};
|
|
7372
|
-
if (!isRecord(state) || state.version !== 1) throw new ServeStoreError("invalid_state", `serve.json version ${String(isRecord(state) ? state.version : void 0)} is not supported`);
|
|
7673
|
+
if (!isRecord$1(state) || state.version !== 1) throw new ServeStoreError("invalid_state", `serve.json version ${String(isRecord$1(state) ? state.version : void 0)} is not supported`);
|
|
7373
7674
|
if ("pairedOrigins" in state) throw new ServeStoreError("invalid_state", "serve.json uses the obsolete pairing format; clear the unreleased serve store and reconfigure it");
|
|
7374
|
-
if (!isRecord(state.pendingRegistrations) || !isRecord(state.activations)) throw new ServeStoreError("invalid_state", "serve.json is missing the version 1 activation map; clear the unreleased serve store and reconfigure it");
|
|
7675
|
+
if (!isRecord$1(state.pendingRegistrations) || !isRecord$1(state.activations)) throw new ServeStoreError("invalid_state", "serve.json is missing the version 1 activation map; clear the unreleased serve store and reconfigure it");
|
|
7375
7676
|
for (const [alias, activation] of Object.entries(state.activations)) validateActivation(alias, activation);
|
|
7376
7677
|
for (const [alias, registration] of Object.entries(state.pendingRegistrations)) {
|
|
7377
7678
|
assertStoreName("agent name", alias);
|
|
7378
|
-
if (!isRecord(registration) || typeof registration.apiUrl !== "string" || registration.apiUrl.length === 0 || typeof registration.createdAt !== "string" || registration.createdAt.length === 0) throw new ServeStoreError("invalid_state", `pending registration "${alias}" is not valid`);
|
|
7679
|
+
if (!isRecord$1(registration) || typeof registration.apiUrl !== "string" || registration.apiUrl.length === 0 || typeof registration.createdAt !== "string" || registration.createdAt.length === 0) throw new ServeStoreError("invalid_state", `pending registration "${alias}" is not valid`);
|
|
7379
7680
|
}
|
|
7380
7681
|
return {
|
|
7381
7682
|
version: 1,
|
|
@@ -7549,7 +7850,7 @@ function directoryBytes(path) {
|
|
|
7549
7850
|
for (const entry of readdirSync(path, { withFileTypes: true })) total += directoryBytes(join(path, entry.name));
|
|
7550
7851
|
return total;
|
|
7551
7852
|
}
|
|
7552
|
-
function isRecord(value) {
|
|
7853
|
+
function isRecord$1(value) {
|
|
7553
7854
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7554
7855
|
}
|
|
7555
7856
|
function storeChildPath(root, kind, value, suffix = "") {
|
|
@@ -7567,7 +7868,7 @@ function validateActivation(alias, value) {
|
|
|
7567
7868
|
const invalid = () => {
|
|
7568
7869
|
throw new ServeStoreError("invalid_state", `activation "${alias}" is not a valid version 1 activation`);
|
|
7569
7870
|
};
|
|
7570
|
-
if (!isRecord(value)) invalid();
|
|
7871
|
+
if (!isRecord$1(value)) invalid();
|
|
7571
7872
|
const activation = value;
|
|
7572
7873
|
if (activation.alias !== alias) invalid();
|
|
7573
7874
|
if (![
|
|
@@ -7612,8 +7913,9 @@ function reserveAlias(store, alias) {
|
|
|
7612
7913
|
pending.add(alias);
|
|
7613
7914
|
return () => pending.delete(alias);
|
|
7614
7915
|
}
|
|
7615
|
-
async function createManagedAgent(store, secrets, input) {
|
|
7916
|
+
async function createManagedAgent(store, secrets, input, connectAgent = connect) {
|
|
7616
7917
|
const alias = assertStoreName("agent name", input.name);
|
|
7918
|
+
if (!input.enrollmentToken.trim()) throw new ServeIdentityError("enrollment_required", "an enrollment token from the target team is required — a self-registered agent would be stranded in its own personal team");
|
|
7617
7919
|
const releaseAlias = reserveAlias(store, alias);
|
|
7618
7920
|
let registeredIdentityId;
|
|
7619
7921
|
try {
|
|
@@ -7627,7 +7929,7 @@ async function createManagedAgent(store, secrets, input) {
|
|
|
7627
7929
|
const result = await register({
|
|
7628
7930
|
credentialType: "agent_key",
|
|
7629
7931
|
apiUrl,
|
|
7630
|
-
|
|
7932
|
+
enrollmentToken: input.enrollmentToken,
|
|
7631
7933
|
signal: boundedIdentitySignal(input.signal)
|
|
7632
7934
|
});
|
|
7633
7935
|
if (result.credentials.type !== "agent_key") throw new ServeIdentityError("unsupported_credential", `registration returned credential type "${result.credentials.type}"; serve manages agent-key credentials only`);
|
|
@@ -7656,22 +7958,34 @@ async function createManagedAgent(store, secrets, input) {
|
|
|
7656
7958
|
mcp: deriveMcpUrl(result.apiUrl)
|
|
7657
7959
|
}
|
|
7658
7960
|
};
|
|
7961
|
+
store.writeAgentConfig(alias, config);
|
|
7962
|
+
await secrets.write(agentKeyReference.key, result.credentials.secret);
|
|
7963
|
+
await secrets.write(seedReference.key, privateKey);
|
|
7964
|
+
const whoami = await callWhoami(connectAgent, {
|
|
7965
|
+
agentKey: result.credentials.secret,
|
|
7966
|
+
apiUrl: result.apiUrl
|
|
7967
|
+
}, store.agentPath(alias), input.signal);
|
|
7968
|
+
assertIdentityMatches(whoami, {
|
|
7969
|
+
identityId,
|
|
7970
|
+
publicKey,
|
|
7971
|
+
fingerprint
|
|
7972
|
+
}, "authenticated whoami", `new managed agent "${alias}"`);
|
|
7973
|
+
const boundTeamId = boundTeamIdFromWhoami(whoami);
|
|
7659
7974
|
const activation = {
|
|
7660
7975
|
alias,
|
|
7661
7976
|
source: "managed",
|
|
7662
7977
|
identityId,
|
|
7663
7978
|
publicKey,
|
|
7664
7979
|
fingerprint,
|
|
7980
|
+
...boundTeamId ? { boundTeamId } : {},
|
|
7665
7981
|
createdAt: now,
|
|
7666
7982
|
apiUrl: result.apiUrl
|
|
7667
7983
|
};
|
|
7668
|
-
store.writeAgentConfig(alias, config);
|
|
7669
|
-
await secrets.write(agentKeyReference.key, result.credentials.secret);
|
|
7670
|
-
await secrets.write(seedReference.key, privateKey);
|
|
7671
7984
|
store.writeActivation(activation);
|
|
7672
7985
|
return {
|
|
7673
7986
|
activation,
|
|
7674
|
-
config
|
|
7987
|
+
config,
|
|
7988
|
+
...boundTeamId ? { boundTeamId } : {}
|
|
7675
7989
|
};
|
|
7676
7990
|
} catch (cause) {
|
|
7677
7991
|
if (!registeredIdentityId && cause instanceof MoltNetError && cause.statusCode !== void 0 && cause.statusCode >= 400 && cause.statusCode < 500) {
|
|
@@ -7708,21 +8022,25 @@ async function reconcileManagedRegistration(store, secrets, aliasInput, action,
|
|
|
7708
8022
|
if (!agentKey || privateKeyState !== "present") throw new ServeIdentityError("registration_incomplete", `pending registration for "${alias}" is missing persisted secret material`);
|
|
7709
8023
|
const identity = identityFromConfig(config);
|
|
7710
8024
|
const apiUrl = requireConfigApiUrl(config, store.agentPath(alias));
|
|
7711
|
-
|
|
8025
|
+
const whoami = await callWhoami(connectAgent, {
|
|
7712
8026
|
agentKey,
|
|
7713
8027
|
apiUrl
|
|
7714
|
-
}, store.agentPath(alias), signal)
|
|
8028
|
+
}, store.agentPath(alias), signal);
|
|
8029
|
+
assertIdentityMatches(whoami, identity, "authenticated whoami", `pending registration "${alias}" config`);
|
|
8030
|
+
const boundTeamId = boundTeamIdFromWhoami(whoami);
|
|
7715
8031
|
const recovered = {
|
|
7716
8032
|
alias,
|
|
7717
8033
|
source: "managed",
|
|
7718
8034
|
...identity,
|
|
8035
|
+
...boundTeamId ? { boundTeamId } : {},
|
|
7719
8036
|
createdAt: config.registered_at,
|
|
7720
8037
|
apiUrl
|
|
7721
8038
|
};
|
|
7722
8039
|
store.writeActivation(recovered);
|
|
7723
8040
|
return {
|
|
7724
8041
|
activation: recovered,
|
|
7725
|
-
config
|
|
8042
|
+
config,
|
|
8043
|
+
...recovered.boundTeamId ? { boundTeamId: recovered.boundTeamId } : {}
|
|
7726
8044
|
};
|
|
7727
8045
|
}
|
|
7728
8046
|
async function attachExternalAgent(store, secretProviders, input, connectAgent = connect) {
|
|
@@ -7738,10 +8056,12 @@ async function attachExternalAgent(store, secretProviders, input, connectAgent =
|
|
|
7738
8056
|
const whoami = await authenticateConfig(input.configDir, effectiveApiUrl, secretProviders, connectAgent, input.signal);
|
|
7739
8057
|
const identity = identityFromConfig(config);
|
|
7740
8058
|
assertIdentityMatches(identity, whoami, `external config ${configPath}`, "authenticated whoami");
|
|
8059
|
+
const boundTeamId = boundTeamIdFromWhoami(whoami);
|
|
7741
8060
|
const activation = {
|
|
7742
8061
|
alias,
|
|
7743
8062
|
source: "external",
|
|
7744
8063
|
...identity,
|
|
8064
|
+
...boundTeamId ? { boundTeamId } : {},
|
|
7745
8065
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7746
8066
|
configPath,
|
|
7747
8067
|
configApiUrl,
|
|
@@ -7750,7 +8070,8 @@ async function attachExternalAgent(store, secretProviders, input, connectAgent =
|
|
|
7750
8070
|
store.writeActivation(activation);
|
|
7751
8071
|
return {
|
|
7752
8072
|
activation,
|
|
7753
|
-
config
|
|
8073
|
+
config,
|
|
8074
|
+
...activation.boundTeamId ? { boundTeamId: activation.boundTeamId } : {}
|
|
7754
8075
|
};
|
|
7755
8076
|
} finally {
|
|
7756
8077
|
releaseAlias();
|
|
@@ -7761,9 +8082,12 @@ async function verifyAgentActivation(store, alias, managedSecretProviders, exter
|
|
|
7761
8082
|
const activation = requireActivation(store, alias);
|
|
7762
8083
|
const verified = activation.source === "managed" ? await verifyManagedActivation(store, activation, managedSecretProviders, connectAgent, signal) : await verifyExternalActivation(activation, externalSecretProviders, connectAgent, signal);
|
|
7763
8084
|
assertIdentityMatches(verified.whoami, activation, "authenticated whoami", `agent "${activation.alias}" pinned activation`);
|
|
8085
|
+
const boundTeamId = boundTeamIdFromWhoami(verified.whoami);
|
|
8086
|
+
if (activation.boundTeamId !== boundTeamId) throw new ServeIdentityError("verification_failed", `authenticated whoami team binding does not match agent "${activation.alias}" pinned activation`);
|
|
7764
8087
|
return {
|
|
7765
8088
|
activation,
|
|
7766
|
-
config: verified.config
|
|
8089
|
+
config: verified.config,
|
|
8090
|
+
...boundTeamId ? { boundTeamId } : {}
|
|
7767
8091
|
};
|
|
7768
8092
|
}
|
|
7769
8093
|
async function verifyManagedActivation(store, activation, secretProviders, connectAgent, signal) {
|
|
@@ -7902,6 +8226,7 @@ function publicAgentView(store, activation) {
|
|
|
7902
8226
|
agentName: activation.alias,
|
|
7903
8227
|
identityId: activation.identityId,
|
|
7904
8228
|
fingerprint: activation.fingerprint,
|
|
8229
|
+
...activation.boundTeamId ? { teamId: activation.boundTeamId } : {},
|
|
7905
8230
|
apiUrl: activation.apiUrl,
|
|
7906
8231
|
createdAt: activation.createdAt,
|
|
7907
8232
|
hasAgentKey: Boolean(config?.agent_key_ref),
|
|
@@ -7915,9 +8240,13 @@ function publicAgentView(store, activation) {
|
|
|
7915
8240
|
...activation.apiUrl ? { apiUrl: activation.apiUrl } : {},
|
|
7916
8241
|
identityId: activation.identityId,
|
|
7917
8242
|
fingerprint: activation.fingerprint,
|
|
8243
|
+
...activation.boundTeamId ? { teamId: activation.boundTeamId } : {},
|
|
7918
8244
|
createdAt: activation.createdAt
|
|
7919
8245
|
};
|
|
7920
8246
|
}
|
|
8247
|
+
function boundTeamIdFromWhoami(whoami) {
|
|
8248
|
+
return whoami.credentialBinding?.bindingScope === "team" ? whoami.credentialBinding.boundTeamId ?? void 0 : void 0;
|
|
8249
|
+
}
|
|
7921
8250
|
function requireActivation(store, alias) {
|
|
7922
8251
|
const activation = store.readActivation(alias);
|
|
7923
8252
|
if (!activation) throw new ServeStoreError("not_found", `agent "${alias}" is not configured`);
|
|
@@ -8087,6 +8416,7 @@ var RunManager = class {
|
|
|
8087
8416
|
this.assertStartOpen(signal);
|
|
8088
8417
|
const agent = await (this.options.verifyActivationImpl ?? verifyAgentActivation)(this.store, spec.agent, this.options.secretProviders, this.options.externalSecretProviders, void 0, signal);
|
|
8089
8418
|
this.assertStartOpen(signal);
|
|
8419
|
+
if (agent.boundTeamId && agent.boundTeamId !== spec.teamId) throw new ServeRunError("invalid_spec", `agent "${spec.agent}" has a key bound to team ${agent.boundTeamId}; start the run with that team, or create a new agent with an enrollment token from team ${spec.teamId}`);
|
|
8090
8420
|
const id = `${Date.now().toString(36)}-${randomBytes(4).toString("hex")}`;
|
|
8091
8421
|
const runDir = this.store.runDir(id);
|
|
8092
8422
|
const piDir = join(runDir, "pi");
|
|
@@ -8116,6 +8446,11 @@ var RunManager = class {
|
|
|
8116
8446
|
models: provider.models
|
|
8117
8447
|
}]))
|
|
8118
8448
|
});
|
|
8449
|
+
try {
|
|
8450
|
+
(this.options.symlinkImpl ?? symlinkSync)(this.store.piAuthJsonPath, join(piDir, "auth.json"));
|
|
8451
|
+
} catch (cause) {
|
|
8452
|
+
throw new ServeStoreError("io_error", "could not link subscription credentials into the run", { cause });
|
|
8453
|
+
}
|
|
8119
8454
|
const entry = this.entrypoint();
|
|
8120
8455
|
logStream = createWriteStream(logPath, {
|
|
8121
8456
|
fd: openSync(logPath, "a", 384),
|
|
@@ -8470,6 +8805,62 @@ async function withServeLock(root, work, options) {
|
|
|
8470
8805
|
await held.release();
|
|
8471
8806
|
}
|
|
8472
8807
|
}
|
|
8808
|
+
var ServeModelDiscoveryError = class extends Error {
|
|
8809
|
+
name = "ServeModelDiscoveryError";
|
|
8810
|
+
constructor(code, message, statusCode, options) {
|
|
8811
|
+
super(message, options);
|
|
8812
|
+
this.code = code;
|
|
8813
|
+
this.statusCode = statusCode;
|
|
8814
|
+
}
|
|
8815
|
+
};
|
|
8816
|
+
var ModelDiscoveryCollector = class {
|
|
8817
|
+
models = /* @__PURE__ */ new Set();
|
|
8818
|
+
addOpenAiResponse(value) {
|
|
8819
|
+
if (!isRecord(value) || !Array.isArray(value["data"])) return;
|
|
8820
|
+
for (const candidate of value["data"]) {
|
|
8821
|
+
if (!isRecord(candidate)) continue;
|
|
8822
|
+
const id = candidate["id"];
|
|
8823
|
+
if (typeof id === "string" && id.length > 0) this.models.add(id);
|
|
8824
|
+
}
|
|
8825
|
+
}
|
|
8826
|
+
addOllamaResponse(value) {
|
|
8827
|
+
if (!isRecord(value) || !Array.isArray(value["models"])) return;
|
|
8828
|
+
for (const candidate of value["models"]) {
|
|
8829
|
+
if (!isRecord(candidate)) continue;
|
|
8830
|
+
const name = candidate["name"];
|
|
8831
|
+
if (typeof name === "string" && name.length > 0) this.models.add(name);
|
|
8832
|
+
}
|
|
8833
|
+
}
|
|
8834
|
+
get size() {
|
|
8835
|
+
return this.models.size;
|
|
8836
|
+
}
|
|
8837
|
+
result(providerId, failures) {
|
|
8838
|
+
if (this.models.size === 0) throw discoveryFailure(providerId, failures);
|
|
8839
|
+
return {
|
|
8840
|
+
models: [...this.models].sort().slice(0, 500),
|
|
8841
|
+
discoveredCount: this.models.size
|
|
8842
|
+
};
|
|
8843
|
+
}
|
|
8844
|
+
};
|
|
8845
|
+
function parseProviderBaseUrl(value, providerId) {
|
|
8846
|
+
let parsed;
|
|
8847
|
+
try {
|
|
8848
|
+
parsed = new URL(value);
|
|
8849
|
+
} catch (cause) {
|
|
8850
|
+
throw new ServeModelDiscoveryError("invalid_provider", `provider "${providerId}" has an invalid base URL`, 400, { cause });
|
|
8851
|
+
}
|
|
8852
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:" || parsed.username || parsed.password || parsed.search || parsed.hash) throw new ServeModelDiscoveryError("invalid_provider", `provider "${providerId}" base URL must be HTTP(S) without credentials, query, or fragment`, 400);
|
|
8853
|
+
return parsed;
|
|
8854
|
+
}
|
|
8855
|
+
function discoveryFailure(providerId, failures) {
|
|
8856
|
+
if (failures.some((failure) => failure.kind === "http" && (failure.status === 401 || failure.status === 403))) return new ServeModelDiscoveryError("discovery_unauthorized", `provider "${providerId}" rejected model discovery; check its API key`, 502);
|
|
8857
|
+
if (failures.some((failure) => failure.kind === "network")) return new ServeModelDiscoveryError("discovery_unavailable", `provider "${providerId}" could not be reached for model discovery`, 502);
|
|
8858
|
+
if (failures.some((failure) => failure.kind === "invalid_response")) return new ServeModelDiscoveryError("discovery_invalid_response", `provider "${providerId}" returned an invalid model response`, 502);
|
|
8859
|
+
return new ServeModelDiscoveryError("discovery_failed", `no models discovered for provider "${providerId}"`, 502);
|
|
8860
|
+
}
|
|
8861
|
+
function isRecord(value) {
|
|
8862
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8863
|
+
}
|
|
8473
8864
|
//#endregion
|
|
8474
8865
|
//#region src/lib/serve/server.ts
|
|
8475
8866
|
/**
|
|
@@ -8562,9 +8953,9 @@ function optionalString(body, field) {
|
|
|
8562
8953
|
if (typeof value !== "string" || value.trim().length === 0) throw new ServeHttpError(400, "invalid_body", `"${field}" must be a non-empty string when present`);
|
|
8563
8954
|
return value.trim();
|
|
8564
8955
|
}
|
|
8565
|
-
function stringArray(body, field) {
|
|
8956
|
+
function stringArray(body, field, options = {}) {
|
|
8566
8957
|
const value = body[field];
|
|
8567
|
-
if (!Array.isArray(value) || value.length === 0 || value.some((item) => typeof item !== "string" || item.length === 0)) throw new ServeHttpError(400, "invalid_body", `"${field}" must be a non-empty string array`);
|
|
8958
|
+
if (!Array.isArray(value) || !options.allowEmpty && value.length === 0 || value.some((item) => typeof item !== "string" || item.length === 0)) throw new ServeHttpError(400, "invalid_body", `"${field}" must be ${options.allowEmpty ? "a" : "a non-empty"} string array`);
|
|
8568
8959
|
return value;
|
|
8569
8960
|
}
|
|
8570
8961
|
function requestOperationSignal(request, shutdownSignal) {
|
|
@@ -8622,7 +9013,11 @@ function buildServeServer(options) {
|
|
|
8622
9013
|
registerStatusRoute(app, options, requirePairedOrigin);
|
|
8623
9014
|
registerAgentRoutes(app, options, requirePairedOrigin);
|
|
8624
9015
|
registerProviderRoutes(app, options, requirePairedOrigin);
|
|
9016
|
+
registerSubscriptionRoutes(app, options, requirePairedOrigin);
|
|
8625
9017
|
registerRunRoutes(app, options, requirePairedOrigin);
|
|
9018
|
+
app.addHook("onClose", () => {
|
|
9019
|
+
options.subscriptions.close();
|
|
9020
|
+
});
|
|
8626
9021
|
app.setNotFoundHandler(async (_request, reply) => reply.code(404).send({
|
|
8627
9022
|
code: "not_found",
|
|
8628
9023
|
message: "Route is not available"
|
|
@@ -8680,6 +9075,7 @@ function registerStatusRoute(app, options, requirePairedOrigin) {
|
|
|
8680
9075
|
return {
|
|
8681
9076
|
version: options.version,
|
|
8682
9077
|
platform: process.platform,
|
|
9078
|
+
subscriptions: options.subscriptions.list(),
|
|
8683
9079
|
agents: store.listActivations().map((activation) => publicAgentView(store, activation)),
|
|
8684
9080
|
providers: Object.fromEntries(Object.entries(store.readProviders()).map(([id, provider]) => [id, providerView(provider)])),
|
|
8685
9081
|
runs: runViews(runs)
|
|
@@ -8698,11 +9094,11 @@ function registerAgentRoutes(app, options, requirePairedOrigin) {
|
|
|
8698
9094
|
const signal = requestOperationSignal(request, options.shutdownSignal);
|
|
8699
9095
|
const kind = requireString(body, "kind");
|
|
8700
9096
|
if (kind === "managed") {
|
|
8701
|
-
|
|
9097
|
+
if (body["apiUrl"] !== void 0) throw new ServeHttpError(400, "invalid_body", "managed agent registration uses the configured MoltNet API URL; apiUrl cannot be overridden");
|
|
8702
9098
|
const entry = await createManagedAgent(store, options.secrets, {
|
|
8703
9099
|
name: requireString(body, "name"),
|
|
8704
9100
|
apiUrl: options.defaultApiUrl,
|
|
8705
|
-
|
|
9101
|
+
enrollmentToken: requireString(body, "enrollmentToken"),
|
|
8706
9102
|
signal
|
|
8707
9103
|
});
|
|
8708
9104
|
return reply.code(201).send(publicAgentView(store, entry.activation));
|
|
@@ -8740,16 +9136,106 @@ function registerProviderRoutes(app, options, requirePairedOrigin) {
|
|
|
8740
9136
|
requirePairedOrigin(request);
|
|
8741
9137
|
return Object.fromEntries(Object.entries(store.readProviders()).map(([id, provider]) => [id, providerView(provider)]));
|
|
8742
9138
|
});
|
|
9139
|
+
app.post("/v1/providers/:providerId/discover-models", async (request) => {
|
|
9140
|
+
requirePairedOrigin(request);
|
|
9141
|
+
const { providerId: rawProviderId } = request.params;
|
|
9142
|
+
const providerId = assertProviderId(rawProviderId);
|
|
9143
|
+
const provider = store.readProviders()[providerId];
|
|
9144
|
+
if (!provider) throw new ServeHttpError(404, "provider_not_found", `provider "${providerId}" was not found`);
|
|
9145
|
+
const parsed = parseProviderBaseUrl(provider.baseUrl, providerId);
|
|
9146
|
+
const baseUrl = parsed.href.replace(/\/$/u, "");
|
|
9147
|
+
let apiKey;
|
|
9148
|
+
if (provider.apiKeyRef) try {
|
|
9149
|
+
apiKey = await options.secretProviders.resolve(parseSecretReferenceString(provider.apiKeyRef));
|
|
9150
|
+
} catch (error) {
|
|
9151
|
+
request.log.warn({
|
|
9152
|
+
...safeErrorContext(error),
|
|
9153
|
+
code: "serve_provider_secret_unavailable",
|
|
9154
|
+
providerId
|
|
9155
|
+
}, "Provider API key could not be resolved for model discovery");
|
|
9156
|
+
throw new ServeHttpError(400, "provider_secret_unavailable", `provider "${providerId}" API key could not be resolved`);
|
|
9157
|
+
}
|
|
9158
|
+
const headers = apiKey ? { authorization: `Bearer ${apiKey}` } : {};
|
|
9159
|
+
const fetchImpl = options.discoverFetch ?? fetch;
|
|
9160
|
+
const failures = [];
|
|
9161
|
+
const collector = new ModelDiscoveryCollector();
|
|
9162
|
+
const tryJson = async (endpoint, url) => {
|
|
9163
|
+
let response;
|
|
9164
|
+
try {
|
|
9165
|
+
response = await fetchImpl(url, {
|
|
9166
|
+
headers,
|
|
9167
|
+
redirect: "error",
|
|
9168
|
+
signal: AbortSignal.timeout(1e4)
|
|
9169
|
+
});
|
|
9170
|
+
} catch (error) {
|
|
9171
|
+
const errorType = error instanceof Error ? error.name : typeof error;
|
|
9172
|
+
failures.push({
|
|
9173
|
+
kind: "network",
|
|
9174
|
+
errorType
|
|
9175
|
+
});
|
|
9176
|
+
request.log.warn({
|
|
9177
|
+
code: "serve_provider_discovery_request_failed",
|
|
9178
|
+
endpoint,
|
|
9179
|
+
errorType,
|
|
9180
|
+
providerId
|
|
9181
|
+
}, "Provider model discovery request failed");
|
|
9182
|
+
return null;
|
|
9183
|
+
}
|
|
9184
|
+
if (!response.ok) {
|
|
9185
|
+
failures.push({
|
|
9186
|
+
kind: "http",
|
|
9187
|
+
status: response.status
|
|
9188
|
+
});
|
|
9189
|
+
const context = {
|
|
9190
|
+
code: "serve_provider_discovery_upstream_error",
|
|
9191
|
+
endpoint,
|
|
9192
|
+
providerId,
|
|
9193
|
+
statusCode: response.status
|
|
9194
|
+
};
|
|
9195
|
+
if (response.status >= 500 || response.status === 401 || response.status === 403) request.log.warn(context, "Provider model discovery was rejected");
|
|
9196
|
+
else request.log.info(context, "Provider model discovery endpoint unavailable");
|
|
9197
|
+
return null;
|
|
9198
|
+
}
|
|
9199
|
+
try {
|
|
9200
|
+
return await response.json();
|
|
9201
|
+
} catch {
|
|
9202
|
+
failures.push({ kind: "invalid_response" });
|
|
9203
|
+
request.log.warn({
|
|
9204
|
+
code: "serve_provider_discovery_invalid_json",
|
|
9205
|
+
endpoint,
|
|
9206
|
+
providerId
|
|
9207
|
+
}, "Provider model discovery returned invalid JSON");
|
|
9208
|
+
return null;
|
|
9209
|
+
}
|
|
9210
|
+
};
|
|
9211
|
+
collector.addOpenAiResponse(await tryJson("openai_models", `${baseUrl}/models`));
|
|
9212
|
+
if (collector.size === 0) collector.addOllamaResponse(await tryJson("ollama_tags", `${parsed.origin}/api/tags`));
|
|
9213
|
+
const result = collector.result(providerId, failures);
|
|
9214
|
+
if (result.discoveredCount > result.models.length) request.log.warn({
|
|
9215
|
+
code: "serve_provider_discovery_truncated",
|
|
9216
|
+
discoveredCount: result.discoveredCount,
|
|
9217
|
+
providerId,
|
|
9218
|
+
returnedCount: 500
|
|
9219
|
+
}, "Provider model discovery result was truncated");
|
|
9220
|
+
request.log.info({
|
|
9221
|
+
code: "serve_provider_discovery_completed",
|
|
9222
|
+
modelCount: result.models.length,
|
|
9223
|
+
providerId
|
|
9224
|
+
}, "Provider model discovery completed");
|
|
9225
|
+
return { models: result.models };
|
|
9226
|
+
});
|
|
8743
9227
|
app.put("/v1/providers/:providerId", async (request, reply) => {
|
|
8744
9228
|
requirePairedOrigin(request);
|
|
8745
9229
|
const { providerId: rawProviderId } = request.params;
|
|
8746
9230
|
const providerId = assertProviderId(rawProviderId);
|
|
8747
9231
|
const body = requireBody(request);
|
|
9232
|
+
const baseUrl = requireString(body, "baseUrl");
|
|
9233
|
+
parseProviderBaseUrl(baseUrl, providerId);
|
|
8748
9234
|
const entry = {
|
|
8749
9235
|
api: requireString(body, "api"),
|
|
8750
|
-
baseUrl
|
|
9236
|
+
baseUrl,
|
|
8751
9237
|
envName: assertProviderEnvName(providerId, requireString(body, "envName")),
|
|
8752
|
-
models: stringArray(body, "models")
|
|
9238
|
+
models: stringArray(body, "models", { allowEmpty: true })
|
|
8753
9239
|
};
|
|
8754
9240
|
const apiKey = optionalString(body, "apiKey");
|
|
8755
9241
|
await serialize(async () => {
|
|
@@ -8774,6 +9260,28 @@ function runViews(runs) {
|
|
|
8774
9260
|
active: runs.isActive(record.id)
|
|
8775
9261
|
}));
|
|
8776
9262
|
}
|
|
9263
|
+
function registerSubscriptionRoutes(app, options, requirePairedOrigin) {
|
|
9264
|
+
app.get("/v1/subscriptions", async (request) => {
|
|
9265
|
+
requirePairedOrigin(request);
|
|
9266
|
+
return options.subscriptions.list();
|
|
9267
|
+
});
|
|
9268
|
+
app.post("/v1/subscriptions/:providerId/login", async (request, reply) => {
|
|
9269
|
+
requirePairedOrigin(request);
|
|
9270
|
+
const { providerId } = request.params;
|
|
9271
|
+
const login = await options.subscriptions.start(providerId);
|
|
9272
|
+
return reply.code(201).send(login);
|
|
9273
|
+
});
|
|
9274
|
+
app.get("/v1/subscriptions/:providerId/login", async (request) => {
|
|
9275
|
+
requirePairedOrigin(request);
|
|
9276
|
+
const { providerId } = request.params;
|
|
9277
|
+
return options.subscriptions.status(providerId);
|
|
9278
|
+
});
|
|
9279
|
+
app.delete("/v1/subscriptions/:providerId/login", async (request) => {
|
|
9280
|
+
requirePairedOrigin(request);
|
|
9281
|
+
const { providerId } = request.params;
|
|
9282
|
+
return options.subscriptions.cancel(providerId);
|
|
9283
|
+
});
|
|
9284
|
+
}
|
|
8777
9285
|
function registerRunRoutes(app, options, requirePairedOrigin) {
|
|
8778
9286
|
const { runs } = options;
|
|
8779
9287
|
app.get("/v1/runs", async (request) => {
|
|
@@ -8944,6 +9452,16 @@ function normalizeServeError(error) {
|
|
|
8944
9452
|
code: error.code,
|
|
8945
9453
|
message: error.message
|
|
8946
9454
|
};
|
|
9455
|
+
if (error instanceof ServeSubscriptionError) return {
|
|
9456
|
+
statusCode: error.code === "login_not_found" ? 404 : error.code === "provider_unknown" ? 404 : 400,
|
|
9457
|
+
code: error.code,
|
|
9458
|
+
message: error.message
|
|
9459
|
+
};
|
|
9460
|
+
if (error instanceof ServeModelDiscoveryError) return {
|
|
9461
|
+
statusCode: error.statusCode,
|
|
9462
|
+
code: error.code,
|
|
9463
|
+
message: error.message
|
|
9464
|
+
};
|
|
8947
9465
|
if (error instanceof ServeIdentityError) return {
|
|
8948
9466
|
statusCode: error.code === "agent_exists" ? 409 : 400,
|
|
8949
9467
|
code: error.code,
|
|
@@ -8952,7 +9470,7 @@ function normalizeServeError(error) {
|
|
|
8952
9470
|
return {
|
|
8953
9471
|
statusCode: 500,
|
|
8954
9472
|
code: "internal_error",
|
|
8955
|
-
message: "
|
|
9473
|
+
message: "The local supervisor could not complete the request."
|
|
8956
9474
|
};
|
|
8957
9475
|
}
|
|
8958
9476
|
//#endregion
|
|
@@ -9009,6 +9527,10 @@ async function runServe(argv) {
|
|
|
9009
9527
|
const externalSecretProviders = createNodeSecretProviderRegistry();
|
|
9010
9528
|
const pairing = new PairingService();
|
|
9011
9529
|
const shutdownController = new AbortController();
|
|
9530
|
+
const subscriptions = new ProviderLoginService({
|
|
9531
|
+
authPath: store.piAuthJsonPath,
|
|
9532
|
+
logger
|
|
9533
|
+
});
|
|
9012
9534
|
const runs = new RunManager({
|
|
9013
9535
|
store,
|
|
9014
9536
|
secretProviders,
|
|
@@ -9019,9 +9541,11 @@ async function runServe(argv) {
|
|
|
9019
9541
|
const app = buildServeServer({
|
|
9020
9542
|
store,
|
|
9021
9543
|
secrets,
|
|
9544
|
+
secretProviders,
|
|
9022
9545
|
externalSecretProviders,
|
|
9023
9546
|
pairing,
|
|
9024
9547
|
runs,
|
|
9548
|
+
subscriptions,
|
|
9025
9549
|
allowedOrigins,
|
|
9026
9550
|
selfOrigin: `http://127.0.0.1:${port}`,
|
|
9027
9551
|
defaultApiUrl,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@themoltnet/agent-daemon",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.49.1",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Universal MoltNet agent daemon host with a built-in Pi/Gondolin runtime and support for trusted operator-owned runtime modules. CLI: moltnet-agent.",
|
|
@@ -48,7 +48,8 @@
|
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@earendil-works/gondolin": "^0.12.0",
|
|
51
|
-
"@earendil-works/pi-
|
|
51
|
+
"@earendil-works/pi-ai": "0.79.4",
|
|
52
|
+
"@earendil-works/pi-coding-agent": "0.79.4",
|
|
52
53
|
"@fastify/cors": "^11.0.0",
|
|
53
54
|
"@fastify/helmet": "^13.0.1",
|
|
54
55
|
"@fastify/otel": "^0.18.0",
|
|
@@ -82,10 +83,10 @@
|
|
|
82
83
|
"pino-pretty": "^13.1.3",
|
|
83
84
|
"proper-lockfile": "4.1.2",
|
|
84
85
|
"typebox": "^1.2.8",
|
|
86
|
+
"@themoltnet/agent-runtime": "0.45.2",
|
|
85
87
|
"@themoltnet/os-keyring": "0.3.0",
|
|
86
|
-
"@themoltnet/
|
|
87
|
-
"@themoltnet/
|
|
88
|
-
"@themoltnet/agent-runtime": "0.45.2"
|
|
88
|
+
"@themoltnet/pi-runtime": "0.14.1",
|
|
89
|
+
"@themoltnet/sdk": "0.140.0"
|
|
89
90
|
},
|
|
90
91
|
"devDependencies": {
|
|
91
92
|
"@types/proper-lockfile": "^4.1.4",
|
|
@@ -95,15 +96,15 @@
|
|
|
95
96
|
"vite-plugin-dts": "^4.5.4",
|
|
96
97
|
"vitest": "^3.0.0",
|
|
97
98
|
"@moltnet/agent-eval": "0.1.0",
|
|
98
|
-
"@moltnet/bootstrap": "0.1.0",
|
|
99
99
|
"@moltnet/crypto-service": "0.1.0",
|
|
100
100
|
"@moltnet/execution-integrations": "0.1.0",
|
|
101
101
|
"@moltnet/execution-plan": "0.1.0",
|
|
102
|
+
"@moltnet/bootstrap": "0.1.0",
|
|
102
103
|
"@moltnet/loopback-companion": "0.1.0",
|
|
104
|
+
"@moltnet/observability": "0.1.0",
|
|
103
105
|
"@moltnet/models": "0.1.0",
|
|
104
|
-
"@moltnet/tasks": "0.1.0",
|
|
105
106
|
"@moltnet/runtime-profiles": "0.1.0",
|
|
106
|
-
"@moltnet/
|
|
107
|
+
"@moltnet/tasks": "0.1.0"
|
|
107
108
|
},
|
|
108
109
|
"nx": {
|
|
109
110
|
"projectType": "application",
|