@themoltnet/agent-daemon 0.55.0 → 0.56.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/README.md +6 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +2020 -1220
- package/package.json +8 -8
package/dist/cli.js
CHANGED
|
@@ -14,7 +14,7 @@ import { AgentRuntime, ApiTaskReporter, ApiTaskSource, PollingApiTaskSource, cre
|
|
|
14
14
|
import { GuestEnvironmentBoundaryError, assertGuestEnvironmentBoundary, createPiRetryTriage, findMainWorktree, isResolvedPathInsideRoot, normalizeRetryTriageResult, redactRetryTriageSecrets, resolveRuntimeProfileModel } from "@themoltnet/pi-runtime";
|
|
15
15
|
import { FILE_SECRET_PROVIDER, FileSecretProvider, connect, createNodeSecretProviderRegistry } from "@themoltnet/sdk/node";
|
|
16
16
|
import { constants, createReadStream, createWriteStream, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
17
|
-
import { AuthenticationError, IDENTITY_ALIAS_PATTERN, MoltNetError, agentKeyKey, assertIdentityAlias, assertTrustedConfigApiUrl, createExecutorAttestor, deriveMcpUrl, formatSecretReferenceString, getConfigDir, getIdentityDir, identitySeedKey, isCanonicalConfig, parseSecretReferenceString, readConfig, register, requireSecureCredentialApiUrl, resolveAgentKey, resolveEnvSecretReference, resolveIdentitySeed
|
|
17
|
+
import { AuthenticationError, IDENTITY_ALIAS_PATTERN, MoltNetError, agentKeyKey, assertIdentityAlias, assertTrustedConfigApiUrl, createExecutorAttestor, deriveMcpUrl, formatSecretReferenceString, getConfigDir, getIdentityDir, identitySeedKey, isCanonicalConfig, parseSecretReferenceString, readConfig, register, requireSecureCredentialApiUrl, resolveAgentKey, resolveEnvSecretReference, resolveIdentitySeed } from "@themoltnet/sdk";
|
|
18
18
|
import { execFile, spawn } from "node:child_process";
|
|
19
19
|
import { X509Certificate, createHash, createPrivateKey, createPublicKey, randomBytes, randomUUID, timingSafeEqual, webcrypto } from "node:crypto";
|
|
20
20
|
import { once } from "node:events";
|
|
@@ -30,17 +30,18 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
30
30
|
import { mkdir, open, readFile, realpath, stat, writeFile } from "node:fs/promises";
|
|
31
31
|
import { pipeline } from "node:stream/promises";
|
|
32
32
|
import { createInterface } from "node:readline/promises";
|
|
33
|
+
import { ModelRuntime, readStoredCredential } from "@earendil-works/pi-coding-agent";
|
|
34
|
+
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
35
|
+
import { lock, lockSync } from "proper-lockfile";
|
|
36
|
+
import { isIP } from "node:net";
|
|
33
37
|
import cors from "@fastify/cors";
|
|
34
38
|
import helmet from "@fastify/helmet";
|
|
35
|
-
import { lock, lockSync } from "proper-lockfile";
|
|
36
|
-
import { ModelRuntime, readStoredCredential } from "@earendil-works/pi-coding-agent";
|
|
37
39
|
import { Transform, Writable } from "node:stream";
|
|
38
40
|
import { writePiConfig } from "@themoltnet/pi-runtime/pi-config";
|
|
39
41
|
import { pathToFileURL } from "node:url";
|
|
40
42
|
import { StringDecoder } from "node:string_decoder";
|
|
41
43
|
import rateLimit from "@fastify/rate-limit";
|
|
42
44
|
import Fastify from "fastify";
|
|
43
|
-
import { isIP } from "node:net";
|
|
44
45
|
import "reflect-metadata";
|
|
45
46
|
import { homedir, platform } from "node:os";
|
|
46
47
|
import { BasicConstraintsExtension, ExtendedKeyUsage, ExtendedKeyUsageExtension, IP, KeyUsageFlags, KeyUsagesExtension, SubjectAlternativeNameExtension, X509CertificateGenerator } from "@peculiar/x509";
|
|
@@ -3458,6 +3459,8 @@ Commands:
|
|
|
3458
3459
|
child processes. Binds 127.0.0.1 only.
|
|
3459
3460
|
server trust
|
|
3460
3461
|
Install the per-user macOS local-HTTPS CA after explicit consent.
|
|
3462
|
+
providers Manage configured endpoints and Pi OAuth subscriptions without
|
|
3463
|
+
starting the Agent Server. See \`agent-daemon providers --help\`.
|
|
3461
3464
|
sync-sessions
|
|
3462
3465
|
Repair durable runtime-session checkpoints from local slot files.
|
|
3463
3466
|
update check
|
|
@@ -3624,6 +3627,24 @@ On macOS, the first interactive run asks to trust a per-user local CA in the
|
|
|
3624
3627
|
login keychain and serves HTTPS. Run \`agent-daemon server trust --remove\` to
|
|
3625
3628
|
remove that exact CA. Linux continues to use the Chromium PNA HTTP path.
|
|
3626
3629
|
`;
|
|
3630
|
+
var PROVIDERS_HELP = `\
|
|
3631
|
+
moltnet-agent providers — manage local model providers.
|
|
3632
|
+
|
|
3633
|
+
Usage:
|
|
3634
|
+
moltnet-agent providers list [--json] [--root <path>]
|
|
3635
|
+
moltnet-agent providers set <id> [--base-url <url>] [--api <pi-api-kind>]
|
|
3636
|
+
[--model <id> ... | --clear-models]
|
|
3637
|
+
[--api-key-stdin | --clear-api-key] [--root <path>]
|
|
3638
|
+
moltnet-agent providers discover <id> [--save] [--json] [--root <path>]
|
|
3639
|
+
moltnet-agent providers remove <id> [--yes] [--root <path>]
|
|
3640
|
+
moltnet-agent providers login <id> [--auth-method <method-id>]
|
|
3641
|
+
[--root <path>]
|
|
3642
|
+
moltnet-agent providers logout <id> [--yes] [--root <path>]
|
|
3643
|
+
|
|
3644
|
+
The default root is ~/.config/moltnet. MOLTNET_AGENT_SERVER_ROOT remains the
|
|
3645
|
+
environment override. API keys are accepted only from redirected stdin; they
|
|
3646
|
+
are stored separately and providers.json contains only a secret reference.
|
|
3647
|
+
`;
|
|
3627
3648
|
//#endregion
|
|
3628
3649
|
//#region src/lib/identity-pin.ts
|
|
3629
3650
|
/** Compare every pinned field without choosing a caller-specific error type. */
|
|
@@ -3913,7 +3934,8 @@ function loadAgentServerEnvConfig() {
|
|
|
3913
3934
|
allowedOrigins: process.env["MOLTNET_AGENT_SERVER_ALLOWED_ORIGINS"] ?? "",
|
|
3914
3935
|
root: process.env["MOLTNET_AGENT_SERVER_ROOT"] ?? "",
|
|
3915
3936
|
apiUrl: process.env["MOLTNET_API_URL"] ?? "",
|
|
3916
|
-
logLevel: process.env["LOG_LEVEL"] ?? ""
|
|
3937
|
+
logLevel: process.env["LOG_LEVEL"] ?? "",
|
|
3938
|
+
activeIdentity: process.env["MOLTNET_ACTIVE_IDENTITY"] ?? ""
|
|
3917
3939
|
};
|
|
3918
3940
|
}
|
|
3919
3941
|
/** Full process environment for spawned Agent Server run children. */
|
|
@@ -5292,16 +5314,16 @@ function validateTaskTypes(types) {
|
|
|
5292
5314
|
/**
|
|
5293
5315
|
* OTel SDK bootstrap for the agent daemon.
|
|
5294
5316
|
*
|
|
5295
|
-
* const shutdown = await initWorkerOtel({ serviceName,
|
|
5317
|
+
* const shutdown = await initWorkerOtel({ serviceName, agent });
|
|
5296
5318
|
* try { ... } finally { await shutdown(); }
|
|
5297
5319
|
*
|
|
5298
5320
|
* No-op when MOLTNET_OTEL_ENDPOINT is unset — `@opentelemetry/api` falls
|
|
5299
5321
|
* back to no-op tracers and meters, so the runtime + pi extension still
|
|
5300
5322
|
* call into it but nothing is exported.
|
|
5301
5323
|
*/
|
|
5302
|
-
|
|
5324
|
+
function initWorkerOtel(options) {
|
|
5303
5325
|
const endpoint = options.endpoint;
|
|
5304
|
-
if (!endpoint) return async () => {};
|
|
5326
|
+
if (!endpoint) return Promise.resolve(async () => {});
|
|
5305
5327
|
const resource = resourceFromAttributes({
|
|
5306
5328
|
[ATTR_SERVICE_NAME]: options.serviceName,
|
|
5307
5329
|
...options.serviceVersion && { [ATTR_SERVICE_VERSION]: options.serviceVersion },
|
|
@@ -5309,11 +5331,7 @@ async function initWorkerOtel(options) {
|
|
|
5309
5331
|
...options.resourceAttributes
|
|
5310
5332
|
});
|
|
5311
5333
|
let headersFactory;
|
|
5312
|
-
|
|
5313
|
-
if (!agent && options.agentDir) agent = await connect({
|
|
5314
|
-
configDir: options.agentDir,
|
|
5315
|
-
secretProviders: createNodeSecretProviderRegistry()
|
|
5316
|
-
});
|
|
5334
|
+
const agent = options.agent;
|
|
5317
5335
|
if (agent) headersFactory = async () => {
|
|
5318
5336
|
return { Authorization: `Bearer ${await agent.getToken()}` };
|
|
5319
5337
|
};
|
|
@@ -5338,7 +5356,7 @@ async function initWorkerOtel(options) {
|
|
|
5338
5356
|
});
|
|
5339
5357
|
provider.register();
|
|
5340
5358
|
metrics.setGlobalMeterProvider(meterProvider);
|
|
5341
|
-
return async () => {
|
|
5359
|
+
return Promise.resolve(async () => {
|
|
5342
5360
|
await Promise.all([provider.forceFlush().catch((err) => {
|
|
5343
5361
|
process.stderr.write(`[otel-bootstrap] trace forceFlush failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
5344
5362
|
}), meterProvider.forceFlush().catch((err) => {
|
|
@@ -5349,7 +5367,7 @@ async function initWorkerOtel(options) {
|
|
|
5349
5367
|
}), meterProvider.shutdown().catch((err) => {
|
|
5350
5368
|
process.stderr.write(`[otel-bootstrap] metric shutdown failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
5351
5369
|
})]);
|
|
5352
|
-
};
|
|
5370
|
+
});
|
|
5353
5371
|
}
|
|
5354
5372
|
//#endregion
|
|
5355
5373
|
//#region src/lib/pi-agent-dir.ts
|
|
@@ -6086,11 +6104,12 @@ async function runPolling(opts) {
|
|
|
6086
6104
|
throw error;
|
|
6087
6105
|
}
|
|
6088
6106
|
})();
|
|
6107
|
+
const daemonRootDir = explicitAgentRootDir ?? process.cwd();
|
|
6089
6108
|
const resolvedProfiles = await resolveRuntimeProfiles({
|
|
6090
6109
|
agent: ctx.agent,
|
|
6091
6110
|
profiles: profileValues,
|
|
6092
6111
|
teamId,
|
|
6093
|
-
cwd:
|
|
6112
|
+
cwd: daemonRootDir
|
|
6094
6113
|
});
|
|
6095
6114
|
const runtimeAdapter = opts.runtimeAdapter ?? defaultPiDaemonAdapter;
|
|
6096
6115
|
const preparedRuntimes = /* @__PURE__ */ new Map();
|
|
@@ -6743,11 +6762,12 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
|
|
|
6743
6762
|
throw error;
|
|
6744
6763
|
}
|
|
6745
6764
|
})();
|
|
6765
|
+
const daemonRootDir = explicitAgentRootDir ?? process.cwd();
|
|
6746
6766
|
const profile = await resolveRuntimeProfile({
|
|
6747
6767
|
agent: ctx.agent,
|
|
6748
6768
|
profile: values.profile,
|
|
6749
6769
|
teamId: values.team,
|
|
6750
|
-
cwd:
|
|
6770
|
+
cwd: daemonRootDir
|
|
6751
6771
|
});
|
|
6752
6772
|
assertRuntimeAdapterSupportsProfile(runtimeAdapter, profile);
|
|
6753
6773
|
const preparedRuntime = attestPreparedRuntime(await runtimeAdapter.prepare({ profile }), signingPrivateKey);
|
|
@@ -7110,1115 +7130,2051 @@ function runPoll(argv, runtimeAdapter) {
|
|
|
7110
7130
|
runtimeAdapter
|
|
7111
7131
|
});
|
|
7112
7132
|
}
|
|
7113
|
-
|
|
7114
|
-
|
|
7115
|
-
|
|
7116
|
-
|
|
7117
|
-
|
|
7118
|
-
|
|
7119
|
-
|
|
7133
|
+
var NAME_RE = IDENTITY_ALIAS_PATTERN;
|
|
7134
|
+
var PROVIDER_ID_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
7135
|
+
function assertStoreName(kind, value) {
|
|
7136
|
+
try {
|
|
7137
|
+
return assertIdentityAlias(value);
|
|
7138
|
+
} catch {
|
|
7139
|
+
throw new AgentServerStoreError("invalid_name", `${kind} must match ${NAME_RE.source}`);
|
|
7120
7140
|
}
|
|
7121
|
-
};
|
|
7122
|
-
function isLoopbackViolation(error) {
|
|
7123
|
-
return error instanceof LoopbackViolationError;
|
|
7124
7141
|
}
|
|
7125
|
-
|
|
7126
|
-
|
|
7127
|
-
|
|
7128
|
-
function isLoopbackHostname(hostname) {
|
|
7129
|
-
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
|
|
7142
|
+
function assertProviderId(value) {
|
|
7143
|
+
if (!PROVIDER_ID_RE.test(value)) throw new AgentServerStoreError("invalid_name", `provider id must match ${PROVIDER_ID_RE.source}`);
|
|
7144
|
+
return value;
|
|
7130
7145
|
}
|
|
7146
|
+
var AgentServerStoreError = class extends Error {
|
|
7147
|
+
name = "AgentServerStoreError";
|
|
7148
|
+
constructor(code, message, options) {
|
|
7149
|
+
super(message, options);
|
|
7150
|
+
this.code = code;
|
|
7151
|
+
}
|
|
7152
|
+
};
|
|
7131
7153
|
/**
|
|
7132
|
-
*
|
|
7133
|
-
*
|
|
7134
|
-
*
|
|
7135
|
-
*
|
|
7154
|
+
* `MOLTNET_AGENT_SERVER_ROOT` override, else `~/.config/moltnet`.
|
|
7155
|
+
*
|
|
7156
|
+
* Deliberately does NOT consult `XDG_CONFIG_HOME`. The Go CLI's GetConfigDir
|
|
7157
|
+
* and @moltnet/agent-config's getConfigDir both resolve `~/.config/moltnet`,
|
|
7158
|
+
* so honouring XDG here gave one application two config roots: on a machine
|
|
7159
|
+
* with the variable set, the daemon wrote identities the CLI and SDK could not
|
|
7160
|
+
* read. `MOLTNET_AGENT_SERVER_ROOT` remains the explicit escape hatch for a
|
|
7161
|
+
* genuinely custom location.
|
|
7136
7162
|
*/
|
|
7137
|
-
function
|
|
7138
|
-
|
|
7163
|
+
function resolveAgentServerRoot(input) {
|
|
7164
|
+
const override = input.root?.trim();
|
|
7165
|
+
if (override) return override;
|
|
7166
|
+
return getConfigDir();
|
|
7167
|
+
}
|
|
7168
|
+
function providerEnvName(providerId) {
|
|
7169
|
+
return `MOLTNET_PROVIDER_${assertProviderId(providerId).replaceAll("-", "_").toUpperCase()}_API_KEY`;
|
|
7170
|
+
}
|
|
7171
|
+
function assertProviderEnvName(providerId, value) {
|
|
7172
|
+
const expected = providerEnvName(providerId);
|
|
7173
|
+
if (value !== expected) throw new AgentServerStoreError("invalid_state", `provider envName must be ${expected}`);
|
|
7174
|
+
return value;
|
|
7175
|
+
}
|
|
7176
|
+
function readJson(path) {
|
|
7177
|
+
let raw;
|
|
7139
7178
|
try {
|
|
7140
|
-
|
|
7179
|
+
raw = readFileSync(path, "utf8");
|
|
7141
7180
|
} catch (cause) {
|
|
7142
|
-
|
|
7181
|
+
if (cause.code === "ENOENT") return null;
|
|
7182
|
+
throw new AgentServerStoreError("io_error", `could not read state at ${path}`, { cause });
|
|
7183
|
+
}
|
|
7184
|
+
try {
|
|
7185
|
+
return JSON.parse(raw);
|
|
7186
|
+
} catch (cause) {
|
|
7187
|
+
throw new AgentServerStoreError("invalid_state", `corrupt JSON at ${path}`, { cause });
|
|
7143
7188
|
}
|
|
7144
|
-
if (url.origin !== value || url.protocol !== "https:" && !(url.protocol === "http:" && isLoopbackHostname(url.hostname))) throw new LoopbackViolationError("origin_invalid", "Origin is not valid");
|
|
7145
|
-
return url.origin;
|
|
7146
7189
|
}
|
|
7147
|
-
|
|
7148
|
-
|
|
7149
|
-
|
|
7190
|
+
function writeJsonAtomic(path, value) {
|
|
7191
|
+
const temp = `${path}.${randomBytes(6).toString("hex")}.tmp`;
|
|
7192
|
+
try {
|
|
7193
|
+
writeFileSync(temp, `${JSON.stringify(value, null, 2)}\n`, { mode: 384 });
|
|
7194
|
+
renameSync(temp, path);
|
|
7195
|
+
} catch (cause) {
|
|
7196
|
+
try {
|
|
7197
|
+
rmSync(temp, { force: true });
|
|
7198
|
+
} catch {}
|
|
7199
|
+
throw cause;
|
|
7200
|
+
}
|
|
7150
7201
|
}
|
|
7151
|
-
|
|
7152
|
-
|
|
7153
|
-
|
|
7154
|
-
|
|
7155
|
-
|
|
7156
|
-
|
|
7157
|
-
|
|
7158
|
-
|
|
7159
|
-
this.
|
|
7202
|
+
var AgentServerStore = class {
|
|
7203
|
+
root;
|
|
7204
|
+
identitiesDir;
|
|
7205
|
+
runsDir;
|
|
7206
|
+
secretsDir;
|
|
7207
|
+
/** Shared Pi credential dir; `auth.json` inside is pi-managed (lockfiled). */
|
|
7208
|
+
piDir;
|
|
7209
|
+
constructor(root) {
|
|
7210
|
+
this.root = root;
|
|
7211
|
+
this.identitiesDir = join(root, "identities");
|
|
7212
|
+
this.runsDir = join(root, "runs");
|
|
7213
|
+
this.secretsDir = join(root, "secrets");
|
|
7214
|
+
this.piDir = join(root, "pi");
|
|
7160
7215
|
}
|
|
7161
|
-
|
|
7162
|
-
|
|
7163
|
-
|
|
7164
|
-
|
|
7165
|
-
|
|
7216
|
+
get piAuthJsonPath() {
|
|
7217
|
+
return join(this.piDir, "auth.json");
|
|
7218
|
+
}
|
|
7219
|
+
/** Create the directory layout (0700) if missing. Idempotent. */
|
|
7220
|
+
ensure() {
|
|
7221
|
+
for (const dir of [
|
|
7222
|
+
this.root,
|
|
7223
|
+
this.identitiesDir,
|
|
7224
|
+
this.runsDir,
|
|
7225
|
+
this.secretsDir
|
|
7226
|
+
]) mkdirSync(dir, {
|
|
7227
|
+
recursive: true,
|
|
7228
|
+
mode: 448
|
|
7229
|
+
});
|
|
7230
|
+
return this;
|
|
7231
|
+
}
|
|
7232
|
+
get statePath() {
|
|
7233
|
+
return join(this.root, "agent-server.json");
|
|
7234
|
+
}
|
|
7235
|
+
readAgentServerState() {
|
|
7236
|
+
const state = readJson(this.statePath);
|
|
7237
|
+
if (!state) return {
|
|
7238
|
+
version: 2,
|
|
7239
|
+
pendingRegistrations: {},
|
|
7240
|
+
activations: {}
|
|
7241
|
+
};
|
|
7242
|
+
if (!isRecord$1(state) || state.version !== 2) throw new AgentServerStoreError("invalid_state", `agent-server.json version ${String(isRecord$1(state) ? state.version : void 0)} is not supported; move agent-server.json aside, run \`moltnet config migrate\`, then add or attach the agents again`);
|
|
7243
|
+
if ("pairedOrigins" in state) throw new AgentServerStoreError("invalid_state", "agent-server.json uses the obsolete pairing format; move agent-server.json aside and configure the agent server again");
|
|
7244
|
+
if (!isRecord$1(state.pendingRegistrations) || !isRecord$1(state.activations)) throw new AgentServerStoreError("invalid_state", "agent-server.json is missing the version 2 activation map; move agent-server.json aside, run `moltnet config migrate`, then add or attach the agents again");
|
|
7245
|
+
for (const [alias, activation] of Object.entries(state.activations)) validateActivation(alias, activation);
|
|
7246
|
+
for (const [alias, registration] of Object.entries(state.pendingRegistrations)) {
|
|
7247
|
+
assertStoreName("agent name", alias);
|
|
7248
|
+
if (!isRecord$1(registration) || typeof registration.apiUrl !== "string" || registration.apiUrl.length === 0 || typeof registration.createdAt !== "string" || registration.createdAt.length === 0) throw new AgentServerStoreError("invalid_state", `pending registration "${alias}" is not valid`);
|
|
7166
7249
|
}
|
|
7250
|
+
return {
|
|
7251
|
+
version: 2,
|
|
7252
|
+
pendingRegistrations: state.pendingRegistrations,
|
|
7253
|
+
activations: state.activations
|
|
7254
|
+
};
|
|
7167
7255
|
}
|
|
7168
|
-
|
|
7169
|
-
|
|
7170
|
-
|
|
7256
|
+
writeAgentServerState(state) {
|
|
7257
|
+
writeJsonAtomic(this.statePath, state);
|
|
7258
|
+
}
|
|
7259
|
+
agentPath(name) {
|
|
7260
|
+
return join(storeChildPath(this.identitiesDir, "identity alias", name), "moltnet.json");
|
|
7261
|
+
}
|
|
7262
|
+
/** Central identity directory, shared with the Go CLI layout. */
|
|
7263
|
+
identityDir(name) {
|
|
7264
|
+
return storeChildPath(this.identitiesDir, "identity alias", name);
|
|
7265
|
+
}
|
|
7266
|
+
get identitySelectorPath() {
|
|
7267
|
+
return join(this.root, "identity-selector.json");
|
|
7268
|
+
}
|
|
7269
|
+
readIdentitySelector() {
|
|
7270
|
+
const selector = readJson(this.identitySelectorPath);
|
|
7271
|
+
if (!selector) return null;
|
|
7272
|
+
if (!isRecord$1(selector) || selector.version !== 1 || selector.default_identity !== void 0 && typeof selector.default_identity !== "string") throw new AgentServerStoreError("invalid_state", "identity-selector.json is not a supported selector document");
|
|
7273
|
+
if (selector.default_identity) assertStoreName("identity alias", selector.default_identity);
|
|
7274
|
+
return selector;
|
|
7275
|
+
}
|
|
7276
|
+
writeIdentitySelector(alias) {
|
|
7277
|
+
writeJsonAtomic(this.identitySelectorPath, {
|
|
7278
|
+
version: 1,
|
|
7279
|
+
default_identity: assertStoreName("identity alias", alias)
|
|
7280
|
+
});
|
|
7281
|
+
}
|
|
7282
|
+
/** Identity aliases available in the shared user-level store. */
|
|
7283
|
+
listIdentityAliases() {
|
|
7284
|
+
return readdirSync(this.identitiesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && IDENTITY_ALIAS_PATTERN.test(entry.name)).map((entry) => entry.name).sort((left, right) => left.localeCompare(right));
|
|
7285
|
+
}
|
|
7286
|
+
resolveIdentityAlias(explicit, active) {
|
|
7287
|
+
const alias = explicit?.trim() || active?.trim() || this.readIdentitySelector()?.default_identity;
|
|
7288
|
+
if (!alias) throw new AgentServerStoreError("not_found", "no active identity selected");
|
|
7289
|
+
return assertStoreName("identity alias", alias);
|
|
7290
|
+
}
|
|
7291
|
+
readAgentConfig(alias) {
|
|
7292
|
+
return readJson(this.agentPath(alias));
|
|
7293
|
+
}
|
|
7294
|
+
writeAgentConfig(alias, config) {
|
|
7295
|
+
mkdirSync(this.identityDir(alias), {
|
|
7296
|
+
recursive: true,
|
|
7297
|
+
mode: 448
|
|
7298
|
+
});
|
|
7299
|
+
writeJsonAtomic(this.agentPath(alias), config);
|
|
7300
|
+
if (!this.readIdentitySelector()?.default_identity) this.writeIdentitySelector(alias);
|
|
7301
|
+
}
|
|
7302
|
+
removeAgentConfig(alias) {
|
|
7303
|
+
rmSync(this.agentPath(alias), { force: true });
|
|
7304
|
+
this.clearIdentitySelectorIfDefault(alias);
|
|
7305
|
+
}
|
|
7306
|
+
/** Clears the persisted default when it names `alias`. */
|
|
7307
|
+
clearIdentitySelectorIfDefault(alias) {
|
|
7308
|
+
let selector = null;
|
|
7171
7309
|
try {
|
|
7172
|
-
|
|
7310
|
+
selector = this.readIdentitySelector();
|
|
7173
7311
|
} catch {
|
|
7174
|
-
|
|
7312
|
+
return;
|
|
7175
7313
|
}
|
|
7176
|
-
if (!
|
|
7177
|
-
|
|
7314
|
+
if (!selector || selector.default_identity !== alias) return;
|
|
7315
|
+
writeJsonAtomic(this.identitySelectorPath, { version: 1 });
|
|
7178
7316
|
}
|
|
7179
|
-
|
|
7180
|
-
|
|
7181
|
-
function requireOriginHeader(headers) {
|
|
7182
|
-
const origin = headers.origin;
|
|
7183
|
-
if (typeof origin !== "string" || origin.length === 0) throw new LoopbackViolationError("origin_required", "Origin is required");
|
|
7184
|
-
return origin;
|
|
7185
|
-
}
|
|
7186
|
-
//#endregion
|
|
7187
|
-
//#region ../../libs/loopback-companion/src/fastify.ts
|
|
7188
|
-
var CORS_PREFLIGHT_MAX_AGE_SECONDS = 600;
|
|
7189
|
-
var PRIVATE_NETWORK_REQUEST_HEADER = "access-control-request-private-network";
|
|
7190
|
-
var PRIVATE_NETWORK_ALLOW_HEADER = "access-control-allow-private-network";
|
|
7191
|
-
/**
|
|
7192
|
-
* Enforce that the `Host` header identifies loopback. Blocks DNS-rebinding
|
|
7193
|
-
* setups where a public hostname resolves to 127.0.0.1: the browser then
|
|
7194
|
-
* sends that hostname as `Host`, and the request is refused here even
|
|
7195
|
-
* though the socket is loopback.
|
|
7196
|
-
*/
|
|
7197
|
-
function requireLoopbackHost(request) {
|
|
7198
|
-
const host = request.headers.host;
|
|
7199
|
-
if (!host) throw new LoopbackViolationError("host_required", "Host header is required");
|
|
7200
|
-
let hostname;
|
|
7201
|
-
try {
|
|
7202
|
-
hostname = new URL(`http://${host}`).hostname;
|
|
7203
|
-
} catch {
|
|
7204
|
-
throw new LoopbackViolationError("host_not_loopback", "Host header must identify loopback");
|
|
7317
|
+
readActivation(alias) {
|
|
7318
|
+
return this.readAgentServerState().activations[assertStoreName("agent name", alias)] ?? null;
|
|
7205
7319
|
}
|
|
7206
|
-
|
|
7207
|
-
|
|
7208
|
-
|
|
7209
|
-
|
|
7210
|
-
|
|
7211
|
-
|
|
7212
|
-
|
|
7213
|
-
|
|
7214
|
-
|
|
7215
|
-
|
|
7216
|
-
|
|
7217
|
-
|
|
7218
|
-
|
|
7219
|
-
|
|
7220
|
-
|
|
7221
|
-
|
|
7222
|
-
|
|
7223
|
-
|
|
7224
|
-
|
|
7225
|
-
|
|
7226
|
-
|
|
7227
|
-
|
|
7228
|
-
|
|
7229
|
-
|
|
7230
|
-
if (
|
|
7231
|
-
|
|
7232
|
-
}
|
|
7233
|
-
|
|
7234
|
-
|
|
7320
|
+
hasPendingRegistration(alias) {
|
|
7321
|
+
return Boolean(this.readAgentServerState().pendingRegistrations[assertStoreName("agent name", alias)]);
|
|
7322
|
+
}
|
|
7323
|
+
reserveRegistration(alias, apiUrl) {
|
|
7324
|
+
const name = assertStoreName("agent name", alias);
|
|
7325
|
+
const state = this.readAgentServerState();
|
|
7326
|
+
if (state.activations[name] || state.pendingRegistrations[name]) throw new AgentServerStoreError("already_exists", `agent "${name}" already exists in the agent server store`);
|
|
7327
|
+
state.pendingRegistrations[name] = {
|
|
7328
|
+
apiUrl,
|
|
7329
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7330
|
+
};
|
|
7331
|
+
this.writeAgentServerState(state);
|
|
7332
|
+
}
|
|
7333
|
+
clearPendingRegistration(alias) {
|
|
7334
|
+
const name = assertStoreName("agent name", alias);
|
|
7335
|
+
const state = this.readAgentServerState();
|
|
7336
|
+
delete state.pendingRegistrations[name];
|
|
7337
|
+
this.writeAgentServerState(state);
|
|
7338
|
+
}
|
|
7339
|
+
writeActivation(activation) {
|
|
7340
|
+
const alias = assertStoreName("agent name", activation.alias);
|
|
7341
|
+
validateActivation(alias, activation);
|
|
7342
|
+
const state = this.readAgentServerState();
|
|
7343
|
+
state.activations[alias] = activation;
|
|
7344
|
+
if (activation.source === "managed") delete state.pendingRegistrations[alias];
|
|
7345
|
+
this.writeAgentServerState(state);
|
|
7346
|
+
}
|
|
7347
|
+
listActivations() {
|
|
7348
|
+
return Object.values(this.readAgentServerState().activations).sort((a, b) => a.alias.localeCompare(b.alias));
|
|
7349
|
+
}
|
|
7350
|
+
get providersPath() {
|
|
7351
|
+
return join(this.root, "providers.json");
|
|
7352
|
+
}
|
|
7353
|
+
readProviders() {
|
|
7354
|
+
const state = readJson(this.providersPath) ?? {};
|
|
7355
|
+
this.validateProviders(state);
|
|
7356
|
+
return state;
|
|
7357
|
+
}
|
|
7358
|
+
writeProviders(state) {
|
|
7359
|
+
this.validateProviders(state);
|
|
7360
|
+
writeJsonAtomic(this.providersPath, state);
|
|
7361
|
+
}
|
|
7362
|
+
validateProviders(state) {
|
|
7363
|
+
for (const [id, provider] of Object.entries(state)) {
|
|
7364
|
+
assertProviderId(id);
|
|
7365
|
+
assertProviderEnvName(id, provider.envName);
|
|
7366
|
+
}
|
|
7367
|
+
}
|
|
7368
|
+
runDir(id) {
|
|
7369
|
+
return storeChildPath(this.runsDir, "run id", id);
|
|
7370
|
+
}
|
|
7371
|
+
resolveRunLogPath(id) {
|
|
7372
|
+
let root;
|
|
7373
|
+
let runDir;
|
|
7235
7374
|
try {
|
|
7236
|
-
|
|
7237
|
-
|
|
7375
|
+
root = realpathSync(this.runsDir);
|
|
7376
|
+
runDir = realpathSync(this.runDir(id));
|
|
7238
7377
|
} catch (cause) {
|
|
7239
|
-
|
|
7378
|
+
throw new AgentServerStoreError("io_error", "could not resolve run directory", { cause });
|
|
7240
7379
|
}
|
|
7241
|
-
|
|
7242
|
-
|
|
7243
|
-
|
|
7244
|
-
|
|
7245
|
-
|
|
7246
|
-
"
|
|
7247
|
-
|
|
7248
|
-
|
|
7249
|
-
|
|
7250
|
-
origin: (origin, callback) => {
|
|
7251
|
-
if (!origin || origin === "null") {
|
|
7252
|
-
callback(null, false);
|
|
7253
|
-
return;
|
|
7254
|
-
}
|
|
7255
|
-
if (isOriginAllowed(origin)) {
|
|
7256
|
-
callback(null, true);
|
|
7257
|
-
return;
|
|
7258
|
-
}
|
|
7259
|
-
callback(new LoopbackViolationError("origin_not_allowed", "Origin is not allowed"), false);
|
|
7380
|
+
if (!isStrictDescendant(root, runDir)) throw new AgentServerStoreError("invalid_state", "run directory escapes its store");
|
|
7381
|
+
const logPath = join(runDir, "daemon.log");
|
|
7382
|
+
try {
|
|
7383
|
+
if (lstatSync(logPath).isSymbolicLink()) throw new AgentServerStoreError("invalid_state", "run log must not be a symbolic link");
|
|
7384
|
+
const resolvedLog = realpathSync(logPath);
|
|
7385
|
+
if (!isStrictDescendant(runDir, resolvedLog)) throw new AgentServerStoreError("invalid_state", "run log escapes its store");
|
|
7386
|
+
} catch (cause) {
|
|
7387
|
+
if (cause instanceof AgentServerStoreError) throw cause;
|
|
7388
|
+
if (cause.code !== "ENOENT") throw new AgentServerStoreError("io_error", "could not resolve run log", { cause });
|
|
7260
7389
|
}
|
|
7261
|
-
|
|
7262
|
-
|
|
7263
|
-
|
|
7264
|
-
|
|
7265
|
-
|
|
7266
|
-
|
|
7267
|
-
|
|
7268
|
-
|
|
7269
|
-
|
|
7270
|
-
|
|
7390
|
+
return logPath;
|
|
7391
|
+
}
|
|
7392
|
+
createRunDir(id) {
|
|
7393
|
+
const dir = this.runDir(id);
|
|
7394
|
+
const piDir = join(dir, "pi");
|
|
7395
|
+
mkdirSync(piDir, {
|
|
7396
|
+
recursive: true,
|
|
7397
|
+
mode: 448
|
|
7398
|
+
});
|
|
7399
|
+
return {
|
|
7400
|
+
dir,
|
|
7401
|
+
piDir,
|
|
7402
|
+
logPath: join(dir, "daemon.log")
|
|
7403
|
+
};
|
|
7404
|
+
}
|
|
7405
|
+
readRun(id) {
|
|
7406
|
+
return readJson(join(this.runDir(id), "run.json"));
|
|
7407
|
+
}
|
|
7408
|
+
writeRun(record) {
|
|
7409
|
+
writeJsonAtomic(join(this.runDir(record.id), "run.json"), record);
|
|
7410
|
+
}
|
|
7411
|
+
listRuns(limit = Number.POSITIVE_INFINITY) {
|
|
7412
|
+
let ids;
|
|
7413
|
+
try {
|
|
7414
|
+
ids = readdirSync(this.runsDir);
|
|
7415
|
+
} catch {
|
|
7416
|
+
return [];
|
|
7417
|
+
}
|
|
7418
|
+
const sortedIds = ids.filter((id) => NAME_RE.test(id)).sort().reverse();
|
|
7419
|
+
const selectedIds = Number.isFinite(limit) ? sortedIds.slice(0, Math.max(0, limit)) : sortedIds;
|
|
7420
|
+
const records = [];
|
|
7421
|
+
for (const id of selectedIds) {
|
|
7422
|
+
const record = this.readRun(id);
|
|
7423
|
+
if (record) records.push(record);
|
|
7424
|
+
}
|
|
7425
|
+
return records.sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
7426
|
+
}
|
|
7427
|
+
/** Remove only completed run directories outside the configured budget. */
|
|
7428
|
+
pruneCompletedRuns(options) {
|
|
7429
|
+
const now = (options.now ?? /* @__PURE__ */ new Date()).getTime();
|
|
7430
|
+
let retainedBytes = 0;
|
|
7431
|
+
let retainedCount = 0;
|
|
7432
|
+
const removed = [];
|
|
7433
|
+
for (const record of this.listRuns()) {
|
|
7434
|
+
if (record.status === "running") continue;
|
|
7435
|
+
const dir = this.runDir(record.id);
|
|
7436
|
+
const bytes = directoryBytes(dir);
|
|
7437
|
+
const endedAt = Date.parse(record.endedAt ?? record.startedAt);
|
|
7438
|
+
const expired = !Number.isFinite(endedAt) || now - endedAt > options.maxAgeMs;
|
|
7439
|
+
const overCount = retainedCount >= options.maxCount;
|
|
7440
|
+
const overBytes = retainedBytes + bytes > options.maxBytes;
|
|
7441
|
+
if (expired || overCount || overBytes) {
|
|
7442
|
+
rmSync(dir, {
|
|
7443
|
+
recursive: true,
|
|
7444
|
+
force: true
|
|
7445
|
+
});
|
|
7446
|
+
removed.push(record.id);
|
|
7447
|
+
continue;
|
|
7271
7448
|
}
|
|
7272
|
-
|
|
7273
|
-
|
|
7274
|
-
|
|
7275
|
-
|
|
7276
|
-
|
|
7277
|
-
|
|
7278
|
-
|
|
7449
|
+
retainedCount += 1;
|
|
7450
|
+
retainedBytes += bytes;
|
|
7451
|
+
}
|
|
7452
|
+
return removed;
|
|
7453
|
+
}
|
|
7454
|
+
};
|
|
7455
|
+
function directoryBytes(path) {
|
|
7456
|
+
let info;
|
|
7457
|
+
try {
|
|
7458
|
+
info = lstatSync(path);
|
|
7459
|
+
} catch {
|
|
7460
|
+
return 0;
|
|
7461
|
+
}
|
|
7462
|
+
if (info.isSymbolicLink()) return 0;
|
|
7463
|
+
if (!info.isDirectory()) return info.size;
|
|
7464
|
+
let total = 0;
|
|
7465
|
+
for (const entry of readdirSync(path, { withFileTypes: true })) total += directoryBytes(join(path, entry.name));
|
|
7466
|
+
return total;
|
|
7279
7467
|
}
|
|
7280
|
-
|
|
7281
|
-
|
|
7282
|
-
function headerValue(headers, name) {
|
|
7283
|
-
const value = headers[name];
|
|
7284
|
-
return typeof value === "string" ? value : void 0;
|
|
7468
|
+
function isRecord$1(value) {
|
|
7469
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7285
7470
|
}
|
|
7286
|
-
|
|
7287
|
-
|
|
7288
|
-
|
|
7289
|
-
|
|
7290
|
-
|
|
7291
|
-
|
|
7292
|
-
const mode = headerValue(headers, "sec-fetch-mode");
|
|
7293
|
-
const destination = headerValue(headers, "sec-fetch-dest");
|
|
7294
|
-
if (site !== "cross-site" && site !== "same-origin" && site !== "none" || mode !== "navigate" || destination !== "document") throw new LoopbackViolationError("navigation_required", "Request must be opened as a browser navigation");
|
|
7471
|
+
function storeChildPath(root, kind, value, suffix = "") {
|
|
7472
|
+
const name = assertStoreName(kind, value);
|
|
7473
|
+
const normalizedRoot = resolve(root);
|
|
7474
|
+
const candidate = resolve(normalizedRoot, `${name}${suffix}`);
|
|
7475
|
+
if (!isStrictDescendant(normalizedRoot, candidate)) throw new AgentServerStoreError("invalid_name", `${kind} escapes its store`);
|
|
7476
|
+
return candidate;
|
|
7295
7477
|
}
|
|
7296
|
-
|
|
7297
|
-
|
|
7298
|
-
|
|
7299
|
-
|
|
7300
|
-
|
|
7301
|
-
|
|
7302
|
-
|
|
7303
|
-
|
|
7478
|
+
function isStrictDescendant(root, candidate) {
|
|
7479
|
+
const rootPrefix = root.endsWith(sep) ? root : `${root}${sep}`;
|
|
7480
|
+
return candidate !== root && candidate.startsWith(rootPrefix);
|
|
7481
|
+
}
|
|
7482
|
+
function validateActivation(alias, value) {
|
|
7483
|
+
const invalid = () => {
|
|
7484
|
+
throw new AgentServerStoreError("invalid_state", `activation "${alias}" is not a valid version 2 activation`);
|
|
7485
|
+
};
|
|
7486
|
+
if (!isRecord$1(value)) invalid();
|
|
7487
|
+
const activation = value;
|
|
7488
|
+
if (activation.alias !== alias) invalid();
|
|
7489
|
+
if (![
|
|
7490
|
+
"subjectId",
|
|
7491
|
+
"publicKey",
|
|
7492
|
+
"fingerprint",
|
|
7493
|
+
"createdAt"
|
|
7494
|
+
].every((field) => typeof activation[field] === "string" && activation[field].length > 0)) invalid();
|
|
7495
|
+
if (activation.source === "managed") {
|
|
7496
|
+
if (typeof activation.apiUrl !== "string" || activation.apiUrl.length === 0 || activation.configPath !== void 0 || activation.configApiUrl !== void 0) invalid();
|
|
7497
|
+
return;
|
|
7498
|
+
}
|
|
7499
|
+
if (activation.source !== "external" || typeof activation.configPath !== "string" || activation.configPath.length === 0 || typeof activation.configApiUrl !== "string" || activation.configApiUrl.length === 0 || activation.apiUrl !== void 0 && typeof activation.apiUrl !== "string") invalid();
|
|
7304
7500
|
}
|
|
7305
7501
|
//#endregion
|
|
7306
|
-
//#region src/lib/
|
|
7307
|
-
var
|
|
7308
|
-
|
|
7502
|
+
//#region src/lib/provider-lock.ts
|
|
7503
|
+
var DEFAULT_LOCK_TIMEOUT_MS = 3e4;
|
|
7504
|
+
var LOCK_WAIT_WARNING_MS = 1e3;
|
|
7505
|
+
var ProviderLockError = class extends Error {
|
|
7506
|
+
name = "ProviderLockError";
|
|
7309
7507
|
constructor(code, message, options) {
|
|
7310
7508
|
super(message, options);
|
|
7311
7509
|
this.code = code;
|
|
7312
7510
|
}
|
|
7313
7511
|
};
|
|
7314
7512
|
/**
|
|
7315
|
-
*
|
|
7316
|
-
*
|
|
7317
|
-
*
|
|
7513
|
+
* Serialize provider state shared by the CLI and the long-running Agent Server.
|
|
7514
|
+
* The lock target is a stable directory because the protected JSON files may
|
|
7515
|
+
* not exist yet.
|
|
7318
7516
|
*/
|
|
7319
|
-
async function
|
|
7320
|
-
|
|
7321
|
-
|
|
7322
|
-
|
|
7323
|
-
|
|
7324
|
-
|
|
7325
|
-
|
|
7326
|
-
|
|
7327
|
-
|
|
7328
|
-
|
|
7329
|
-
|
|
7330
|
-
|
|
7331
|
-
|
|
7332
|
-
|
|
7333
|
-
|
|
7334
|
-
|
|
7335
|
-
|
|
7517
|
+
async function withProviderMutationLock(root, work, options = {}) {
|
|
7518
|
+
return withNamedProviderLock(root, "providers", work, options);
|
|
7519
|
+
}
|
|
7520
|
+
/** Serialize Pi credential operations for one provider across processes. */
|
|
7521
|
+
async function withProviderOAuthLock(root, providerId, work, options = {}) {
|
|
7522
|
+
return withNamedProviderLock(root, `oauth-${encodeURIComponent(providerId)}`, work, options);
|
|
7523
|
+
}
|
|
7524
|
+
async function withNamedProviderLock(root, name, work, options) {
|
|
7525
|
+
const locksDir = join(root, "locks");
|
|
7526
|
+
mkdirSync(locksDir, {
|
|
7527
|
+
recursive: true,
|
|
7528
|
+
mode: 448
|
|
7529
|
+
});
|
|
7530
|
+
let compromised;
|
|
7531
|
+
const startedAt = Date.now();
|
|
7532
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS;
|
|
7533
|
+
const lockfilePath = join(locksDir, `${name}.lock`);
|
|
7534
|
+
let warned = false;
|
|
7535
|
+
let release;
|
|
7536
|
+
for (;;) {
|
|
7537
|
+
if (options.signal?.aborted) throw new ProviderLockError("lock_aborted", `provider lock acquisition was cancelled for "${name}"`, { cause: options.signal.reason });
|
|
7538
|
+
const elapsedMs = Date.now() - startedAt;
|
|
7539
|
+
if (elapsedMs >= timeoutMs) throw new ProviderLockError("lock_timeout", `timed out waiting for provider lock "${name}"`);
|
|
7540
|
+
if (!warned && elapsedMs >= LOCK_WAIT_WARNING_MS) {
|
|
7541
|
+
warned = true;
|
|
7542
|
+
options.logger?.warn({
|
|
7543
|
+
code: "provider_lock_contended",
|
|
7544
|
+
elapsedMs,
|
|
7545
|
+
lockName: name
|
|
7546
|
+
}, "Provider operation is waiting for another process");
|
|
7547
|
+
}
|
|
7548
|
+
try {
|
|
7549
|
+
release = await lock(locksDir, {
|
|
7550
|
+
lockfilePath,
|
|
7551
|
+
onCompromised: (error) => {
|
|
7552
|
+
compromised = error;
|
|
7553
|
+
},
|
|
7554
|
+
realpath: false,
|
|
7555
|
+
retries: 0
|
|
7556
|
+
});
|
|
7557
|
+
break;
|
|
7558
|
+
} catch (error) {
|
|
7559
|
+
if (error.code !== "ELOCKED") throw error;
|
|
7560
|
+
const remainingMs = timeoutMs - elapsedMs;
|
|
7561
|
+
try {
|
|
7562
|
+
await setTimeout$1(Math.min(100, remainingMs), void 0, { ...options.signal ? { signal: options.signal } : {} });
|
|
7563
|
+
} catch (cause) {
|
|
7564
|
+
throw new ProviderLockError("lock_aborted", `provider lock acquisition was cancelled for "${name}"`, { cause });
|
|
7336
7565
|
}
|
|
7337
|
-
});
|
|
7338
|
-
} catch (cause) {
|
|
7339
|
-
if (cause.code === "ELOCKED") throw new AgentServerLockError("held", `another moltnet-agent server process already owns ${path}`, { cause });
|
|
7340
|
-
throw new AgentServerLockError("failed", `could not acquire Agent Server lock ${path}: ${cause.message}`, { cause });
|
|
7341
|
-
}
|
|
7342
|
-
let released = false;
|
|
7343
|
-
return {
|
|
7344
|
-
path,
|
|
7345
|
-
async release() {
|
|
7346
|
-
if (released) return;
|
|
7347
|
-
released = true;
|
|
7348
|
-
await releaseLock();
|
|
7349
7566
|
}
|
|
7350
|
-
}
|
|
7351
|
-
|
|
7352
|
-
/** Always release after normal completion or startup/runtime failure. */
|
|
7353
|
-
async function withAgentServerLock(root, work, options) {
|
|
7354
|
-
const held = await acquireAgentServerLock(root, options);
|
|
7567
|
+
}
|
|
7568
|
+
let outcome;
|
|
7355
7569
|
try {
|
|
7356
|
-
|
|
7357
|
-
|
|
7358
|
-
|
|
7570
|
+
outcome = {
|
|
7571
|
+
ok: true,
|
|
7572
|
+
value: await work()
|
|
7573
|
+
};
|
|
7574
|
+
} catch (error) {
|
|
7575
|
+
outcome = {
|
|
7576
|
+
ok: false,
|
|
7577
|
+
error
|
|
7578
|
+
};
|
|
7579
|
+
}
|
|
7580
|
+
try {
|
|
7581
|
+
await release();
|
|
7582
|
+
} catch (error) {
|
|
7583
|
+
if (!compromised && outcome.ok) outcome = {
|
|
7584
|
+
ok: false,
|
|
7585
|
+
error
|
|
7586
|
+
};
|
|
7359
7587
|
}
|
|
7588
|
+
if (!outcome.ok) throw outcome.error;
|
|
7589
|
+
if (compromised) throw compromised;
|
|
7590
|
+
return outcome.value;
|
|
7360
7591
|
}
|
|
7361
7592
|
//#endregion
|
|
7362
|
-
//#region src/lib/
|
|
7363
|
-
|
|
7364
|
-
|
|
7365
|
-
|
|
7366
|
-
|
|
7367
|
-
|
|
7368
|
-
|
|
7369
|
-
|
|
7370
|
-
|
|
7371
|
-
|
|
7372
|
-
|
|
7373
|
-
* 4. Console claims `/v1/pairings/<id>/claim` from the same origin and
|
|
7374
|
-
* receives the bearer token exactly once; only its SHA-256 remains in
|
|
7375
|
-
* this supervisor process.
|
|
7376
|
-
*
|
|
7377
|
-
* The token exists for shared-machine cross-user protection and to bind
|
|
7378
|
-
* "this console session is the operator" — browser-vs-browser isolation is
|
|
7379
|
-
* already covered by the loopback-companion origin checks. Grants are
|
|
7380
|
-
* deliberately process-scoped: after the listening socket changes owners, a
|
|
7381
|
-
* token disclosed to an impostor on that port cannot authenticate to a later
|
|
7382
|
-
* supervisor process.
|
|
7383
|
-
*/
|
|
7384
|
-
var PENDING_TTL_MS = 600 * 1e3;
|
|
7385
|
-
var AgentServerPairingError = class extends Error {
|
|
7386
|
-
name = "AgentServerPairingError";
|
|
7593
|
+
//#region src/lib/oauth-provider.ts
|
|
7594
|
+
var EXCLUDED_OAUTH_PROVIDERS = new Set(["github-copilot"]);
|
|
7595
|
+
function isOAuthProviderEligible(provider) {
|
|
7596
|
+
return provider.auth.oauth !== void 0 && isOAuthProviderIdEligible(provider.id);
|
|
7597
|
+
}
|
|
7598
|
+
function isOAuthProviderIdEligible(providerId) {
|
|
7599
|
+
return !EXCLUDED_OAUTH_PROVIDERS.has(providerId);
|
|
7600
|
+
}
|
|
7601
|
+
var silentLogger$2 = { warn: () => void 0 };
|
|
7602
|
+
var OAuthProviderError = class extends Error {
|
|
7603
|
+
name = "OAuthProviderError";
|
|
7387
7604
|
constructor(code, message) {
|
|
7388
7605
|
super(message);
|
|
7389
7606
|
this.code = code;
|
|
7390
7607
|
}
|
|
7391
7608
|
};
|
|
7392
|
-
|
|
7393
|
-
|
|
7394
|
-
|
|
7395
|
-
|
|
7396
|
-
|
|
7397
|
-
const right = Buffer.from(b, "utf8");
|
|
7398
|
-
return left.length === right.length && timingSafeEqual(left, right);
|
|
7399
|
-
}
|
|
7400
|
-
var PairingService = class {
|
|
7401
|
-
pending = /* @__PURE__ */ new Map();
|
|
7402
|
-
paired = /* @__PURE__ */ new Map();
|
|
7403
|
-
constructor(options = {}) {
|
|
7404
|
-
this.options = options;
|
|
7609
|
+
var OAuthProviderService = class OAuthProviderService {
|
|
7610
|
+
constructor(authPath, runtime, logger) {
|
|
7611
|
+
this.authPath = authPath;
|
|
7612
|
+
this.runtime = runtime;
|
|
7613
|
+
this.logger = logger;
|
|
7405
7614
|
}
|
|
7406
|
-
|
|
7407
|
-
|
|
7615
|
+
static async create(options) {
|
|
7616
|
+
const runtime = options.modelRuntime ?? await ModelRuntime.create({
|
|
7617
|
+
authPath: options.authPath,
|
|
7618
|
+
refreshOnCreate: false
|
|
7619
|
+
});
|
|
7620
|
+
return new OAuthProviderService(options.authPath, runtime, options.logger ?? silentLogger$2);
|
|
7621
|
+
}
|
|
7622
|
+
list() {
|
|
7623
|
+
const providers = this.oauthProviders();
|
|
7624
|
+
const connected = this.connectedProviderIds(providers);
|
|
7625
|
+
return providers.map((provider) => ({
|
|
7626
|
+
id: provider.id,
|
|
7627
|
+
name: provider.name,
|
|
7628
|
+
connected: connected.has(provider.id)
|
|
7629
|
+
}));
|
|
7630
|
+
}
|
|
7631
|
+
async login(providerId, interaction, options = {}) {
|
|
7632
|
+
this.assertProvider(providerId);
|
|
7633
|
+
await withProviderOAuthLock(dirname(this.authPath), providerId, async () => {
|
|
7634
|
+
try {
|
|
7635
|
+
await this.runtime.login(providerId, "oauth", interaction);
|
|
7636
|
+
} finally {
|
|
7637
|
+
await options.onSettledUnderLock?.();
|
|
7638
|
+
}
|
|
7639
|
+
}, {
|
|
7640
|
+
signal: options.signal,
|
|
7641
|
+
logger: this.logger
|
|
7642
|
+
});
|
|
7643
|
+
}
|
|
7644
|
+
async logout(providerId, signal) {
|
|
7645
|
+
this.assertProvider(providerId);
|
|
7646
|
+
await withProviderOAuthLock(dirname(this.authPath), providerId, () => this.runtime.logout(providerId), {
|
|
7647
|
+
signal,
|
|
7648
|
+
logger: this.logger
|
|
7649
|
+
});
|
|
7650
|
+
}
|
|
7651
|
+
assertProvider(providerId) {
|
|
7652
|
+
if (!this.oauthProviders().some((provider) => provider.id === providerId)) throw new OAuthProviderError("provider_unknown", `"${providerId}" is not a known OAuth provider`);
|
|
7653
|
+
}
|
|
7654
|
+
oauthProviders() {
|
|
7655
|
+
return this.runtime.getProviders().filter(isOAuthProviderEligible).map(({ id, name }) => ({
|
|
7656
|
+
id,
|
|
7657
|
+
name
|
|
7658
|
+
}));
|
|
7659
|
+
}
|
|
7660
|
+
connectedProviderIds(providers) {
|
|
7661
|
+
try {
|
|
7662
|
+
const content = readFileSync(this.authPath, "utf8").replace(/^\uFEFF/u, "");
|
|
7663
|
+
const parsed = JSON.parse(content);
|
|
7664
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("Invalid auth.json: expected an object");
|
|
7665
|
+
return new Set(Object.keys(parsed));
|
|
7666
|
+
} catch (error) {
|
|
7667
|
+
if (error.code !== "ENOENT") for (const provider of providers) this.logger.warn({
|
|
7668
|
+
event: "agent-server.subscription_auth_read_failed",
|
|
7669
|
+
providerId: provider.id,
|
|
7670
|
+
...safeOAuthError(error)
|
|
7671
|
+
}, "Could not read subscription authentication state");
|
|
7672
|
+
return /* @__PURE__ */ new Set();
|
|
7673
|
+
}
|
|
7674
|
+
}
|
|
7675
|
+
};
|
|
7676
|
+
function safeOAuthError(error) {
|
|
7677
|
+
const result = { errorType: error instanceof Error ? error.name : typeof error };
|
|
7678
|
+
const code = error?.code;
|
|
7679
|
+
if (typeof code === "string" && /^[a-z0-9_:-]{1,64}$/iu.test(code)) result["applicationCode"] = code;
|
|
7680
|
+
return result;
|
|
7681
|
+
}
|
|
7682
|
+
var AgentServerModelDiscoveryError = class extends Error {
|
|
7683
|
+
name = "AgentServerModelDiscoveryError";
|
|
7684
|
+
constructor(code, message, statusCode, options) {
|
|
7685
|
+
super(message, options);
|
|
7686
|
+
this.code = code;
|
|
7687
|
+
this.statusCode = statusCode;
|
|
7408
7688
|
}
|
|
7409
|
-
|
|
7410
|
-
|
|
7689
|
+
};
|
|
7690
|
+
var ModelDiscoveryCollector = class {
|
|
7691
|
+
models = /* @__PURE__ */ new Set();
|
|
7692
|
+
addOpenAiResponse(value) {
|
|
7693
|
+
if (!isRecord(value) || !Array.isArray(value["data"])) return;
|
|
7694
|
+
for (const candidate of value["data"]) {
|
|
7695
|
+
if (!isRecord(candidate)) continue;
|
|
7696
|
+
const id = candidate["id"];
|
|
7697
|
+
if (typeof id === "string" && id.length > 0) this.models.add(id);
|
|
7698
|
+
}
|
|
7411
7699
|
}
|
|
7412
|
-
|
|
7413
|
-
|
|
7414
|
-
for (const
|
|
7700
|
+
addOllamaResponse(value) {
|
|
7701
|
+
if (!isRecord(value) || !Array.isArray(value["models"])) return;
|
|
7702
|
+
for (const candidate of value["models"]) {
|
|
7703
|
+
if (!isRecord(candidate)) continue;
|
|
7704
|
+
const name = candidate["name"];
|
|
7705
|
+
if (typeof name === "string" && name.length > 0) this.models.add(name);
|
|
7706
|
+
}
|
|
7415
7707
|
}
|
|
7416
|
-
|
|
7417
|
-
this.
|
|
7418
|
-
const pairingId = randomBytes(12).toString("hex");
|
|
7419
|
-
this.pending.set(pairingId, {
|
|
7420
|
-
origin,
|
|
7421
|
-
confirmToken: this.token(),
|
|
7422
|
-
expiresAt: this.now() + PENDING_TTL_MS,
|
|
7423
|
-
approved: false,
|
|
7424
|
-
bearerToken: null
|
|
7425
|
-
});
|
|
7426
|
-
return {
|
|
7427
|
-
pairingId,
|
|
7428
|
-
approvalPath: `/pairings/${pairingId}`
|
|
7429
|
-
};
|
|
7708
|
+
get size() {
|
|
7709
|
+
return this.models.size;
|
|
7430
7710
|
}
|
|
7431
|
-
|
|
7432
|
-
|
|
7433
|
-
const pairing = this.require(pairingId);
|
|
7434
|
-
if (pairing.approved) throw new AgentServerPairingError("pairing_invalid", "Pairing is already approved");
|
|
7711
|
+
result(providerId, failures) {
|
|
7712
|
+
if (this.models.size === 0) throw discoveryFailure(providerId, failures);
|
|
7435
7713
|
return {
|
|
7436
|
-
|
|
7437
|
-
|
|
7714
|
+
models: [...this.models].sort().slice(0, 500),
|
|
7715
|
+
discoveredCount: this.models.size
|
|
7438
7716
|
};
|
|
7439
7717
|
}
|
|
7440
|
-
|
|
7441
|
-
|
|
7442
|
-
|
|
7443
|
-
|
|
7444
|
-
|
|
7445
|
-
|
|
7446
|
-
|
|
7447
|
-
claim(pairingId, origin) {
|
|
7448
|
-
const pairing = this.require(pairingId);
|
|
7449
|
-
if (pairing.origin !== origin) throw new AgentServerPairingError("pairing_origin_mismatch", "Pairing belongs to a different origin");
|
|
7450
|
-
if (!pairing.approved || !pairing.bearerToken) throw new AgentServerPairingError("pairing_not_approved", "Pairing has not been approved yet");
|
|
7451
|
-
const token = pairing.bearerToken;
|
|
7452
|
-
this.pending.delete(pairingId);
|
|
7453
|
-
this.paired.set(origin, sha256Hex(token));
|
|
7454
|
-
return { token };
|
|
7455
|
-
}
|
|
7456
|
-
verify(origin, token) {
|
|
7457
|
-
const tokenHash = this.paired.get(origin);
|
|
7458
|
-
if (!tokenHash || !safeEqual(tokenHash, sha256Hex(token))) throw new AgentServerPairingError("pairing_token_invalid", "Pairing token is not valid for this origin");
|
|
7718
|
+
};
|
|
7719
|
+
function parseProviderBaseUrl(value, providerId) {
|
|
7720
|
+
let parsed;
|
|
7721
|
+
try {
|
|
7722
|
+
parsed = new URL(value);
|
|
7723
|
+
} catch (cause) {
|
|
7724
|
+
throw new AgentServerModelDiscoveryError("invalid_provider", `provider "${providerId}" has an invalid base URL`, 400, { cause });
|
|
7459
7725
|
}
|
|
7460
|
-
|
|
7461
|
-
|
|
7462
|
-
|
|
7463
|
-
|
|
7464
|
-
|
|
7726
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:" || parsed.username || parsed.password || parsed.search || parsed.hash) throw new AgentServerModelDiscoveryError("invalid_provider", `provider "${providerId}" base URL must be HTTP(S) without credentials, query, or fragment`, 400);
|
|
7727
|
+
if (isNonLoopbackPrivateAddress(parsed.hostname)) throw new AgentServerModelDiscoveryError("invalid_provider", `provider "${providerId}" base URL must not target a private network address`, 400);
|
|
7728
|
+
return parsed;
|
|
7729
|
+
}
|
|
7730
|
+
function isNonLoopbackPrivateAddress(hostname) {
|
|
7731
|
+
if (isIP(hostname) !== 4) return false;
|
|
7732
|
+
const [first, second] = hostname.split(".").map(Number);
|
|
7733
|
+
if (first === 127) return false;
|
|
7734
|
+
return first === 10 || first === 172 && second >= 16 && second <= 31 || first === 192 && second === 168 || first === 169 && second === 254;
|
|
7735
|
+
}
|
|
7736
|
+
function discoveryFailure(providerId, failures) {
|
|
7737
|
+
if (failures.some((failure) => failure.kind === "http" && (failure.status === 401 || failure.status === 403))) return new AgentServerModelDiscoveryError("discovery_unauthorized", `provider "${providerId}" rejected model discovery; check its API key`, 502);
|
|
7738
|
+
if (failures.some((failure) => failure.kind === "network")) return new AgentServerModelDiscoveryError("discovery_unavailable", `provider "${providerId}" could not be reached for model discovery`, 502);
|
|
7739
|
+
if (failures.some((failure) => failure.kind === "invalid_response")) return new AgentServerModelDiscoveryError("discovery_invalid_response", `provider "${providerId}" returned an invalid model response`, 502);
|
|
7740
|
+
return new AgentServerModelDiscoveryError("discovery_failed", `no models discovered for provider "${providerId}"`, 502);
|
|
7741
|
+
}
|
|
7742
|
+
function isRecord(value) {
|
|
7743
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7744
|
+
}
|
|
7745
|
+
//#endregion
|
|
7746
|
+
//#region src/lib/safe-error-context.ts
|
|
7747
|
+
function safeErrorContext(error) {
|
|
7748
|
+
const context = { errorType: error instanceof Error ? error.name : typeof error };
|
|
7749
|
+
const applicationCode = safeErrorToken(error?.code);
|
|
7750
|
+
if (applicationCode) context["applicationCode"] = applicationCode;
|
|
7751
|
+
const cause = error instanceof Error ? error.cause : void 0;
|
|
7752
|
+
if (cause instanceof Error) {
|
|
7753
|
+
context["causeType"] = cause.name;
|
|
7754
|
+
const causeMessage = safeLogMessage(cause.message);
|
|
7755
|
+
if (causeMessage) context["causeMessage"] = causeMessage;
|
|
7465
7756
|
}
|
|
7466
|
-
|
|
7467
|
-
|
|
7468
|
-
|
|
7757
|
+
const fsCode = safeErrorToken(cause?.code);
|
|
7758
|
+
const syscall = safeErrorToken(cause?.syscall);
|
|
7759
|
+
if (fsCode) context["fsCode"] = fsCode;
|
|
7760
|
+
if (syscall) context["syscall"] = syscall;
|
|
7761
|
+
const causeStatus = cause?.statusCode;
|
|
7762
|
+
if (typeof causeStatus === "number") context["causeStatusCode"] = causeStatus;
|
|
7763
|
+
return context;
|
|
7469
7764
|
}
|
|
7470
|
-
|
|
7471
|
-
|
|
7472
|
-
return
|
|
7473
|
-
<html lang="en">
|
|
7474
|
-
<head>
|
|
7475
|
-
<meta charset="utf-8" />
|
|
7476
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7477
|
-
<title>MoltNet Agent — approve connection</title>
|
|
7478
|
-
<style>
|
|
7479
|
-
:root { color-scheme: light dark; }
|
|
7480
|
-
body { margin: 0; font: 16px/1.5 system-ui, sans-serif; display: grid; place-items: center; min-height: 100vh; background: Canvas; color: CanvasText; }
|
|
7481
|
-
main { max-width: 26rem; padding: 2rem; border: 1px solid color-mix(in srgb, CanvasText 20%, transparent); border-radius: 12px; }
|
|
7482
|
-
h1 { font-size: 1.2rem; margin: 0 0 0.5rem; }
|
|
7483
|
-
code { font-size: 0.95em; word-break: break-all; }
|
|
7484
|
-
button { margin-top: 1.25rem; font: inherit; padding: 0.6rem 1.4rem; border-radius: 8px; border: 1px solid color-mix(in srgb, CanvasText 30%, transparent); cursor: pointer; }
|
|
7485
|
-
p.small { font-size: 0.85rem; opacity: 0.75; }
|
|
7486
|
-
</style>
|
|
7487
|
-
</head>
|
|
7488
|
-
<body>
|
|
7489
|
-
<script>
|
|
7490
|
-
// The Console must remain this popup's opener until it finishes navigating
|
|
7491
|
-
// from about:blank. Safari rejects that cross-origin navigation otherwise.
|
|
7492
|
-
// Once this trusted local approval document has loaded, it needs no opener.
|
|
7493
|
-
window.opener = null;
|
|
7494
|
-
<\/script>
|
|
7495
|
-
<main>
|
|
7496
|
-
<h1>Allow this site to manage local MoltNet agents?</h1>
|
|
7497
|
-
<p><code>${escapeHtml(input.origin)}</code> asks to configure agents and start or stop local daemon runs on this machine.</p>
|
|
7498
|
-
<p class="small">Approve only if you opened that page yourself. This grant lasts until the local supervisor stops.</p>
|
|
7499
|
-
<form method="post" action="/pairings/${escapeHtml(input.pairingId)}/confirm">
|
|
7500
|
-
<input type="hidden" name="confirmToken" value="${escapeHtml(input.confirmToken)}" />
|
|
7501
|
-
<button type="submit">Approve</button>
|
|
7502
|
-
</form>
|
|
7503
|
-
</main>
|
|
7504
|
-
</body>
|
|
7505
|
-
</html>
|
|
7506
|
-
`;
|
|
7765
|
+
function safeLogMessage(value) {
|
|
7766
|
+
const normalized = value.replace(/[\r\n\t]/gu, " ").trim();
|
|
7767
|
+
return normalized ? normalized.slice(0, 500) : void 0;
|
|
7507
7768
|
}
|
|
7508
|
-
function
|
|
7509
|
-
return
|
|
7510
|
-
<html lang="en">
|
|
7511
|
-
<head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml(input.title)}</title>
|
|
7512
|
-
<style>:root{color-scheme:light dark}body{margin:0;font:16px/1.5 system-ui,sans-serif;display:grid;place-items:center;min-height:100vh;background:Canvas;color:CanvasText}main{max-width:26rem;padding:2rem}</style>
|
|
7513
|
-
</head>
|
|
7514
|
-
<body><main role="status"><h1>${escapeHtml(input.title)}</h1><p>${escapeHtml(input.message)}</p><p>You can close this tab.</p></main></body>
|
|
7515
|
-
</html>
|
|
7516
|
-
`;
|
|
7769
|
+
function safeErrorToken(value) {
|
|
7770
|
+
return typeof value === "string" && /^[a-z0-9_:-]{1,64}$/iu.test(value) ? value : void 0;
|
|
7517
7771
|
}
|
|
7518
7772
|
//#endregion
|
|
7519
|
-
//#region src/lib/
|
|
7520
|
-
|
|
7521
|
-
|
|
7522
|
-
|
|
7523
|
-
|
|
7524
|
-
|
|
7525
|
-
* `pi/auth.json` and token rotation thereafter). The browser only ever sees
|
|
7526
|
-
* the provider's authorize URL or device code — never tokens.
|
|
7527
|
-
*
|
|
7528
|
-
* Provider ids come from Pi's model runtime, so Agent Server stays in lockstep
|
|
7529
|
-
* with supported subscription providers. GitHub Copilot is intentionally
|
|
7530
|
-
* excluded: MoltNet does not broker editor-seat subscription credentials.
|
|
7531
|
-
*/
|
|
7532
|
-
var LOGIN_TTL_MS = 600 * 1e3;
|
|
7533
|
-
/** How long `start()` waits for the flow to surface a URL / device code. */
|
|
7534
|
-
var START_INFO_TIMEOUT_MS = 5e3;
|
|
7535
|
-
var EXCLUDED_SUBSCRIPTION_PROVIDERS = new Set(["github-copilot"]);
|
|
7536
|
-
var AgentServerSubscriptionError = class extends Error {
|
|
7537
|
-
name = "AgentServerSubscriptionError";
|
|
7538
|
-
constructor(code, message) {
|
|
7539
|
-
super(message);
|
|
7773
|
+
//#region src/lib/provider-configuration.ts
|
|
7774
|
+
var DEFAULT_PROVIDER_API = "openai-completions";
|
|
7775
|
+
var ProviderConfigurationError = class extends Error {
|
|
7776
|
+
name = "ProviderConfigurationError";
|
|
7777
|
+
constructor(code, message, statusCode, options) {
|
|
7778
|
+
super(message, options);
|
|
7540
7779
|
this.code = code;
|
|
7780
|
+
this.statusCode = statusCode;
|
|
7541
7781
|
}
|
|
7542
7782
|
};
|
|
7543
|
-
var silentLogger = {
|
|
7783
|
+
var silentLogger$1 = {
|
|
7544
7784
|
info: () => void 0,
|
|
7545
|
-
warn: () => void 0
|
|
7546
|
-
error: () => void 0
|
|
7785
|
+
warn: () => void 0
|
|
7547
7786
|
};
|
|
7548
|
-
var
|
|
7549
|
-
|
|
7787
|
+
var ProviderConfigurationService = class {
|
|
7788
|
+
fetchImpl;
|
|
7550
7789
|
logger;
|
|
7551
7790
|
constructor(options) {
|
|
7552
7791
|
this.options = options;
|
|
7553
|
-
this.
|
|
7792
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
7793
|
+
this.logger = options.logger ?? silentLogger$1;
|
|
7554
7794
|
}
|
|
7555
|
-
|
|
7556
|
-
|
|
7557
|
-
|
|
7558
|
-
|
|
7559
|
-
|
|
7560
|
-
|
|
7561
|
-
|
|
7562
|
-
|
|
7795
|
+
list() {
|
|
7796
|
+
return Object.fromEntries(Object.entries(this.options.store.readProviders()).map(([id, provider]) => [id, providerView(provider)]));
|
|
7797
|
+
}
|
|
7798
|
+
async set(providerIdInput, input, options = {}) {
|
|
7799
|
+
const providerId = assertProviderId(providerIdInput);
|
|
7800
|
+
if (input.apiKey !== void 0 && input.clearApiKey) throw new ProviderConfigurationError("invalid_provider", "apiKey and clearApiKey cannot be used together", 400);
|
|
7801
|
+
if (input.apiKey !== void 0 && input.apiKey.length === 0) throw new ProviderConfigurationError("invalid_provider", "provider API key must not be empty", 400);
|
|
7802
|
+
return withProviderMutationLock(this.options.store.root, async () => {
|
|
7803
|
+
const providers = this.options.store.readProviders();
|
|
7804
|
+
const previous = providers[providerId];
|
|
7805
|
+
const baseUrl = input.baseUrl ?? previous?.baseUrl;
|
|
7806
|
+
if (!baseUrl) throw new ProviderConfigurationError("invalid_provider", `base URL is required when creating provider "${providerId}"`, 400);
|
|
7807
|
+
parseProviderBaseUrl(baseUrl, providerId);
|
|
7808
|
+
const entry = {
|
|
7809
|
+
api: input.api ?? previous?.api ?? DEFAULT_PROVIDER_API,
|
|
7810
|
+
baseUrl,
|
|
7811
|
+
envName: assertProviderEnvName(providerId, input.envName ?? previous?.envName ?? providerEnvName(providerId)),
|
|
7812
|
+
models: [...input.models ?? previous?.models ?? []],
|
|
7813
|
+
...!input.clearApiKey && previous?.apiKeyRef ? { apiKeyRef: previous.apiKeyRef } : {}
|
|
7814
|
+
};
|
|
7815
|
+
const key = `pi-provider/${providerId}`;
|
|
7816
|
+
let previousSecret;
|
|
7817
|
+
const previousKey = managedSecretKey(previous?.apiKeyRef);
|
|
7818
|
+
if ((input.apiKey !== void 0 || input.clearApiKey) && previous?.apiKeyRef) try {
|
|
7819
|
+
previousSecret = await this.options.secretProviders.resolve(parseSecretReferenceString(previous.apiKeyRef));
|
|
7820
|
+
} catch (error) {
|
|
7821
|
+
if (input.clearApiKey) throw new ProviderConfigurationError("provider_secret_unavailable", `provider "${providerId}" API key could not be prepared for removal`, 400, { cause: error });
|
|
7822
|
+
}
|
|
7823
|
+
if (input.apiKey !== void 0) {
|
|
7824
|
+
await this.options.secrets.write(key, input.apiKey);
|
|
7825
|
+
entry.apiKeyRef = formatSecretReferenceString({
|
|
7826
|
+
provider: FILE_SECRET_PROVIDER,
|
|
7827
|
+
key
|
|
7828
|
+
});
|
|
7829
|
+
}
|
|
7830
|
+
if (input.clearApiKey && previousKey) await this.options.secrets.delete(previousKey);
|
|
7831
|
+
try {
|
|
7832
|
+
providers[providerId] = entry;
|
|
7833
|
+
this.options.store.writeProviders(providers);
|
|
7834
|
+
} catch (error) {
|
|
7835
|
+
try {
|
|
7836
|
+
if (input.apiKey !== void 0) if (previousKey === key && previousSecret !== void 0) await this.options.secrets.write(key, previousSecret);
|
|
7837
|
+
else await this.options.secrets.delete(key);
|
|
7838
|
+
else if (input.clearApiKey && previousKey && previousSecret !== void 0) await this.options.secrets.write(previousKey, previousSecret);
|
|
7839
|
+
} catch (rollbackError) {
|
|
7840
|
+
throw new AggregateError([error, rollbackError], "provider update failed and secret rollback was unsuccessful");
|
|
7841
|
+
}
|
|
7842
|
+
throw error;
|
|
7843
|
+
}
|
|
7844
|
+
return providerView(entry);
|
|
7845
|
+
}, {
|
|
7846
|
+
signal: options.signal,
|
|
7847
|
+
logger: this.logger
|
|
7563
7848
|
});
|
|
7564
7849
|
}
|
|
7565
|
-
|
|
7566
|
-
|
|
7850
|
+
async remove(providerIdInput, options = {}) {
|
|
7851
|
+
const providerId = assertProviderId(providerIdInput);
|
|
7852
|
+
await withProviderMutationLock(this.options.store.root, async () => {
|
|
7853
|
+
const providers = this.options.store.readProviders();
|
|
7854
|
+
const provider = providers[providerId];
|
|
7855
|
+
if (!provider) throw new ProviderConfigurationError("provider_not_found", `Provider ${providerId} was not found`, 404);
|
|
7856
|
+
const key = managedSecretKey(provider.apiKeyRef);
|
|
7857
|
+
let previousSecret;
|
|
7858
|
+
if (provider.apiKeyRef) try {
|
|
7859
|
+
previousSecret = await this.options.secretProviders.resolve(parseSecretReferenceString(provider.apiKeyRef));
|
|
7860
|
+
} catch (error) {
|
|
7861
|
+
throw new ProviderConfigurationError("provider_secret_unavailable", `provider "${providerId}" API key could not be prepared for removal`, 400, { cause: error });
|
|
7862
|
+
}
|
|
7863
|
+
if (key) await this.options.secrets.delete(key);
|
|
7864
|
+
delete providers[providerId];
|
|
7865
|
+
try {
|
|
7866
|
+
this.options.store.writeProviders(providers);
|
|
7867
|
+
} catch (error) {
|
|
7868
|
+
if (key && previousSecret !== void 0) try {
|
|
7869
|
+
await this.options.secrets.write(key, previousSecret);
|
|
7870
|
+
} catch (rollbackError) {
|
|
7871
|
+
throw new AggregateError([error, rollbackError], "provider removal failed and secret rollback was unsuccessful");
|
|
7872
|
+
}
|
|
7873
|
+
throw error;
|
|
7874
|
+
}
|
|
7875
|
+
}, {
|
|
7876
|
+
signal: options.signal,
|
|
7877
|
+
logger: this.logger
|
|
7878
|
+
});
|
|
7567
7879
|
}
|
|
7568
|
-
|
|
7569
|
-
|
|
7570
|
-
|
|
7571
|
-
|
|
7572
|
-
|
|
7880
|
+
async discover(providerIdInput, options = {}) {
|
|
7881
|
+
const providerId = assertProviderId(providerIdInput);
|
|
7882
|
+
const provider = this.options.store.readProviders()[providerId];
|
|
7883
|
+
if (!provider) throw new ProviderConfigurationError("provider_not_found", `provider "${providerId}" was not found`, 404);
|
|
7884
|
+
const parsed = parseProviderBaseUrl(provider.baseUrl, providerId);
|
|
7885
|
+
const baseUrl = parsed.href.replace(/\/$/u, "");
|
|
7886
|
+
const apiKey = await this.resolveApiKey(providerId, provider);
|
|
7887
|
+
const headers = apiKey ? { authorization: `Bearer ${apiKey}` } : {};
|
|
7888
|
+
const failures = [];
|
|
7889
|
+
const collector = new ModelDiscoveryCollector();
|
|
7890
|
+
collector.addOpenAiResponse(await this.requestDiscoveryEndpoint({
|
|
7891
|
+
endpoint: "openai_models",
|
|
7892
|
+
failures,
|
|
7893
|
+
headers,
|
|
7894
|
+
providerId,
|
|
7895
|
+
signal: options.signal,
|
|
7896
|
+
url: `${baseUrl}/models`
|
|
7897
|
+
}));
|
|
7898
|
+
if (isOllamaProvider(providerId, parsed)) collector.addOllamaResponse(await this.requestDiscoveryEndpoint({
|
|
7899
|
+
endpoint: "ollama_tags",
|
|
7900
|
+
failures,
|
|
7901
|
+
headers,
|
|
7902
|
+
providerId,
|
|
7903
|
+
signal: options.signal,
|
|
7904
|
+
url: `${parsed.origin}/api/tags`
|
|
7905
|
+
}));
|
|
7906
|
+
const result = collector.result(providerId, failures);
|
|
7907
|
+
if (result.discoveredCount > result.models.length) this.logger.warn({
|
|
7908
|
+
code: "agent_server_provider_discovery_truncated",
|
|
7909
|
+
discoveredCount: result.discoveredCount,
|
|
7910
|
+
providerId,
|
|
7911
|
+
returnedCount: 500
|
|
7912
|
+
}, "Provider model discovery result was truncated");
|
|
7913
|
+
if (options.save) await this.set(providerId, { models: result.models }, options);
|
|
7914
|
+
this.logger.info({
|
|
7915
|
+
code: "agent_server_provider_discovery_completed",
|
|
7916
|
+
modelCount: result.models.length,
|
|
7917
|
+
providerId
|
|
7918
|
+
}, "Provider model discovery completed");
|
|
7919
|
+
return { models: result.models };
|
|
7573
7920
|
}
|
|
7574
|
-
|
|
7575
|
-
if (
|
|
7921
|
+
async resolveApiKey(providerId, provider) {
|
|
7922
|
+
if (!provider.apiKeyRef) return void 0;
|
|
7576
7923
|
try {
|
|
7577
|
-
return
|
|
7924
|
+
return await this.options.secretProviders.resolve(parseSecretReferenceString(provider.apiKeyRef));
|
|
7578
7925
|
} catch (error) {
|
|
7579
7926
|
this.logger.warn({
|
|
7580
|
-
|
|
7581
|
-
|
|
7582
|
-
|
|
7583
|
-
}, "
|
|
7584
|
-
|
|
7585
|
-
}
|
|
7586
|
-
}
|
|
7587
|
-
runtime() {
|
|
7588
|
-
if (!this.options.modelRuntime) throw new Error("ProviderLoginService requires a model runtime when production adapters are not overridden");
|
|
7589
|
-
return this.options.modelRuntime;
|
|
7590
|
-
}
|
|
7591
|
-
list() {
|
|
7592
|
-
this.sweep();
|
|
7593
|
-
return this.providers().map((provider) => ({
|
|
7594
|
-
...provider,
|
|
7595
|
-
connected: this.connected(provider.id)
|
|
7596
|
-
}));
|
|
7597
|
-
}
|
|
7598
|
-
sweep() {
|
|
7599
|
-
const now = this.now();
|
|
7600
|
-
for (const [id, login] of this.logins) if (login.startedAt + LOGIN_TTL_MS <= now) {
|
|
7601
|
-
if (login.status === "pending") this.invalidate(login, "expired");
|
|
7602
|
-
this.logins.delete(id);
|
|
7927
|
+
...safeErrorContext(error),
|
|
7928
|
+
code: "agent_server_provider_secret_unavailable",
|
|
7929
|
+
providerId
|
|
7930
|
+
}, "Provider API key could not be resolved for model discovery");
|
|
7931
|
+
throw new ProviderConfigurationError("provider_secret_unavailable", `provider "${providerId}" API key could not be resolved`, 400, { cause: error });
|
|
7603
7932
|
}
|
|
7604
7933
|
}
|
|
7605
|
-
|
|
7934
|
+
async requestDiscoveryEndpoint(input) {
|
|
7935
|
+
const startedAt = Date.now();
|
|
7936
|
+
const timeout = AbortSignal.timeout(this.options.requestTimeoutMs ?? 1e4);
|
|
7937
|
+
let response;
|
|
7606
7938
|
try {
|
|
7607
|
-
|
|
7608
|
-
|
|
7939
|
+
response = await this.fetchImpl(input.url, {
|
|
7940
|
+
headers: input.headers,
|
|
7941
|
+
redirect: "error",
|
|
7942
|
+
signal: input.signal ? AbortSignal.any([input.signal, timeout]) : timeout
|
|
7943
|
+
});
|
|
7609
7944
|
} catch (error) {
|
|
7610
|
-
|
|
7611
|
-
|
|
7612
|
-
|
|
7613
|
-
providerId: login.providerId,
|
|
7614
|
-
...safeLoginError(error)
|
|
7615
|
-
}, "Could not restore subscription credentials after an invalidated login");
|
|
7616
|
-
return false;
|
|
7617
|
-
}
|
|
7618
|
-
}
|
|
7619
|
-
invalidate(login, transition) {
|
|
7620
|
-
if (login.invalidated) return true;
|
|
7621
|
-
login.invalidated = true;
|
|
7622
|
-
login.abort.abort(/* @__PURE__ */ new Error(`subscription login ${transition}`));
|
|
7623
|
-
const restored = this.restoreCredential(login);
|
|
7624
|
-
login.infoArrived();
|
|
7625
|
-
this.logger.info({
|
|
7626
|
-
event: "agent-server.subscription_login_transition",
|
|
7627
|
-
operationId: login.operationId,
|
|
7628
|
-
providerId: login.providerId,
|
|
7629
|
-
transition
|
|
7630
|
-
}, "Subscription login invalidated");
|
|
7631
|
-
return restored;
|
|
7632
|
-
}
|
|
7633
|
-
status(providerId) {
|
|
7634
|
-
this.sweep();
|
|
7635
|
-
const login = this.logins.get(providerId);
|
|
7636
|
-
if (!login) throw new AgentServerSubscriptionError("login_not_found", `no login in progress for "${providerId}"`);
|
|
7637
|
-
return snapshot(login);
|
|
7638
|
-
}
|
|
7639
|
-
/**
|
|
7640
|
-
* Start (or return the in-flight) login for a provider. Resolves once the
|
|
7641
|
-
* flow has surfaced an authorize URL / device code, completed, or the
|
|
7642
|
-
* start window elapsed — whichever comes first.
|
|
7643
|
-
*/
|
|
7644
|
-
async start(providerId) {
|
|
7645
|
-
this.sweep();
|
|
7646
|
-
if (!this.providers().some((provider) => provider.id === providerId)) throw new AgentServerSubscriptionError("provider_unknown", `"${providerId}" is not a known subscription provider`);
|
|
7647
|
-
const existing = this.logins.get(providerId);
|
|
7648
|
-
if (existing && existing.status === "pending") return snapshot(existing);
|
|
7649
|
-
let infoArrived = () => void 0;
|
|
7650
|
-
const infoPromise = new Promise((resolvePromise) => {
|
|
7651
|
-
infoArrived = () => resolvePromise();
|
|
7652
|
-
});
|
|
7653
|
-
const abort = new AbortController();
|
|
7654
|
-
const login = {
|
|
7655
|
-
providerId,
|
|
7656
|
-
status: "pending",
|
|
7657
|
-
operationId: randomUUID(),
|
|
7658
|
-
startedAt: this.now(),
|
|
7659
|
-
infoArrived,
|
|
7660
|
-
abort,
|
|
7661
|
-
invalidated: false,
|
|
7662
|
-
previousCredential: readStoredCredential(providerId, this.options.authPath)
|
|
7663
|
-
};
|
|
7664
|
-
this.logins.set(providerId, login);
|
|
7665
|
-
this.logger.info({
|
|
7666
|
-
event: "agent-server.subscription_login_transition",
|
|
7667
|
-
operationId: login.operationId,
|
|
7668
|
-
providerId,
|
|
7669
|
-
transition: "started"
|
|
7670
|
-
}, "Subscription login started");
|
|
7671
|
-
const callbacks = createLoginCallbacks(login, this.logger);
|
|
7672
|
-
(this.options.runLogin ?? ((id, loginCallbacks) => this.runtime().login(id, "oauth", toAuthInteraction(loginCallbacks)).then(() => void 0)))(providerId, callbacks).then(() => {
|
|
7673
|
-
if (login.invalidated) {
|
|
7674
|
-
this.restoreCredential(login);
|
|
7675
|
-
return;
|
|
7676
|
-
}
|
|
7677
|
-
if (!this.options.runLogin && !this.connected(providerId)) {
|
|
7678
|
-
login.status = "failed";
|
|
7679
|
-
login.error = "Subscription sign-in completed, but credentials were not persisted. Start again to retry.";
|
|
7680
|
-
this.logger.error({
|
|
7681
|
-
event: "agent-server.subscription_login_transition",
|
|
7682
|
-
operationId: login.operationId,
|
|
7683
|
-
providerId,
|
|
7684
|
-
transition: "persistence_failed"
|
|
7685
|
-
}, "Subscription login credentials were not persisted");
|
|
7686
|
-
} else {
|
|
7687
|
-
login.status = "completed";
|
|
7945
|
+
const elapsedMs = Date.now() - startedAt;
|
|
7946
|
+
if (input.signal?.aborted) {
|
|
7947
|
+
const abortSource = providerAbortSource(input.signal.reason);
|
|
7688
7948
|
this.logger.info({
|
|
7689
|
-
|
|
7690
|
-
|
|
7691
|
-
|
|
7692
|
-
|
|
7693
|
-
|
|
7694
|
-
|
|
7695
|
-
|
|
7696
|
-
}, (error) => {
|
|
7697
|
-
if (login.invalidated) {
|
|
7698
|
-
this.restoreCredential(login);
|
|
7699
|
-
return;
|
|
7949
|
+
abortSource,
|
|
7950
|
+
code: "agent_server_provider_discovery_cancelled",
|
|
7951
|
+
elapsedMs,
|
|
7952
|
+
endpoint: input.endpoint,
|
|
7953
|
+
providerId: input.providerId
|
|
7954
|
+
}, "Provider model discovery was cancelled");
|
|
7955
|
+
throw new ProviderConfigurationError("operation_aborted", `provider "${input.providerId}" discovery was cancelled`, 408, { cause: error });
|
|
7700
7956
|
}
|
|
7701
|
-
|
|
7702
|
-
|
|
7957
|
+
const errorType = error instanceof Error ? error.name : typeof error;
|
|
7958
|
+
input.failures.push({
|
|
7959
|
+
kind: "network",
|
|
7960
|
+
errorType
|
|
7961
|
+
});
|
|
7703
7962
|
this.logger.warn({
|
|
7704
|
-
|
|
7705
|
-
|
|
7706
|
-
|
|
7707
|
-
|
|
7708
|
-
|
|
7709
|
-
|
|
7710
|
-
|
|
7711
|
-
|
|
7712
|
-
|
|
7713
|
-
|
|
7714
|
-
|
|
7715
|
-
|
|
7963
|
+
abortSource: timeout.aborted ? "timeout" : void 0,
|
|
7964
|
+
code: timeout.aborted ? "agent_server_provider_discovery_timeout" : "agent_server_provider_discovery_request_failed",
|
|
7965
|
+
elapsedMs,
|
|
7966
|
+
endpoint: input.endpoint,
|
|
7967
|
+
errorType,
|
|
7968
|
+
providerId: input.providerId
|
|
7969
|
+
}, timeout.aborted ? "Provider model discovery request timed out" : "Provider model discovery request failed");
|
|
7970
|
+
return null;
|
|
7971
|
+
}
|
|
7972
|
+
if (!response.ok) {
|
|
7973
|
+
input.failures.push({
|
|
7974
|
+
kind: "http",
|
|
7975
|
+
status: response.status
|
|
7976
|
+
});
|
|
7977
|
+
const context = {
|
|
7978
|
+
code: "agent_server_provider_discovery_upstream_error",
|
|
7979
|
+
elapsedMs: Date.now() - startedAt,
|
|
7980
|
+
endpoint: input.endpoint,
|
|
7981
|
+
providerId: input.providerId,
|
|
7982
|
+
statusCode: response.status
|
|
7983
|
+
};
|
|
7984
|
+
if (response.status >= 500 || response.status === 401 || response.status === 403) this.logger.warn(context, "Provider model discovery was rejected");
|
|
7985
|
+
else this.logger.info(context, "Provider model discovery endpoint unavailable");
|
|
7986
|
+
return null;
|
|
7987
|
+
}
|
|
7988
|
+
try {
|
|
7989
|
+
return await response.json();
|
|
7990
|
+
} catch {
|
|
7991
|
+
input.failures.push({ kind: "invalid_response" });
|
|
7716
7992
|
this.logger.warn({
|
|
7717
|
-
|
|
7718
|
-
|
|
7719
|
-
|
|
7720
|
-
|
|
7721
|
-
}, "
|
|
7993
|
+
code: "agent_server_provider_discovery_invalid_json",
|
|
7994
|
+
elapsedMs: Date.now() - startedAt,
|
|
7995
|
+
endpoint: input.endpoint,
|
|
7996
|
+
providerId: input.providerId
|
|
7997
|
+
}, "Provider model discovery returned invalid JSON");
|
|
7998
|
+
return null;
|
|
7722
7999
|
}
|
|
7723
|
-
return snapshot(login);
|
|
7724
8000
|
}
|
|
7725
|
-
|
|
7726
|
-
|
|
7727
|
-
|
|
7728
|
-
|
|
7729
|
-
|
|
7730
|
-
|
|
7731
|
-
|
|
7732
|
-
|
|
7733
|
-
|
|
7734
|
-
|
|
8001
|
+
};
|
|
8002
|
+
function providerView(provider) {
|
|
8003
|
+
return {
|
|
8004
|
+
api: provider.api,
|
|
8005
|
+
baseUrl: provider.baseUrl,
|
|
8006
|
+
envName: provider.envName,
|
|
8007
|
+
models: [...provider.models],
|
|
8008
|
+
hasApiKey: Boolean(provider.apiKeyRef)
|
|
8009
|
+
};
|
|
8010
|
+
}
|
|
8011
|
+
function isOllamaProvider(providerId, baseUrl) {
|
|
8012
|
+
return providerId === "ollama" || providerId.startsWith("ollama-") || baseUrl.hostname === "ollama.com" || baseUrl.port === "11434";
|
|
8013
|
+
}
|
|
8014
|
+
function managedSecretKey(reference) {
|
|
8015
|
+
if (!reference) return void 0;
|
|
8016
|
+
try {
|
|
8017
|
+
const parsed = parseSecretReferenceString(reference);
|
|
8018
|
+
return parsed.provider === FILE_SECRET_PROVIDER ? parsed.key : void 0;
|
|
8019
|
+
} catch {
|
|
8020
|
+
return;
|
|
7735
8021
|
}
|
|
7736
|
-
|
|
7737
|
-
|
|
7738
|
-
|
|
7739
|
-
|
|
8022
|
+
}
|
|
8023
|
+
function providerAbortSource(reason) {
|
|
8024
|
+
if (typeof reason === "object" && reason !== null && "source" in reason && reason.source === "shutdown") return "shutdown";
|
|
8025
|
+
return "caller";
|
|
8026
|
+
}
|
|
8027
|
+
//#endregion
|
|
8028
|
+
//#region src/cli/providers.ts
|
|
8029
|
+
async function runProviders(argv, dependencies = {}) {
|
|
8030
|
+
if (isHelpFlag(argv) || argv.length === 0) {
|
|
8031
|
+
(dependencies.stdout ?? console.log)(PROVIDERS_HELP);
|
|
8032
|
+
return argv.length === 0 ? 1 : 0;
|
|
8033
|
+
}
|
|
8034
|
+
let context;
|
|
8035
|
+
try {
|
|
8036
|
+
const [command, ...args] = argv;
|
|
8037
|
+
const parsed = parseProviderArgsSafely(command, args);
|
|
8038
|
+
context = await createContext(parsed.root, dependencies, parsed.command !== "list");
|
|
8039
|
+
switch (parsed.command) {
|
|
8040
|
+
case "list": return listProviders(context, parsed.json);
|
|
8041
|
+
case "set": return await setProvider(context, parsed);
|
|
8042
|
+
case "discover": return await discoverProvider(context, parsed);
|
|
8043
|
+
case "remove": return await removeProvider(context, parsed);
|
|
8044
|
+
case "login": return await loginProvider(context, parsed);
|
|
8045
|
+
case "logout": return await logoutProvider(context, parsed);
|
|
8046
|
+
default: throw new ProviderCliError("invalid_arguments", `Unknown providers command "${String(command)}"`);
|
|
8047
|
+
}
|
|
8048
|
+
} catch (error) {
|
|
8049
|
+
(context?.stderr ?? dependencies.stderr ?? console.error)(publicCliError(error, context?.signal));
|
|
8050
|
+
return 1;
|
|
8051
|
+
} finally {
|
|
8052
|
+
context?.close();
|
|
8053
|
+
}
|
|
8054
|
+
}
|
|
8055
|
+
var ProviderCliError = class extends Error {
|
|
8056
|
+
name = "ProviderCliError";
|
|
8057
|
+
constructor(code, message) {
|
|
8058
|
+
super(message);
|
|
8059
|
+
this.code = code;
|
|
7740
8060
|
}
|
|
7741
8061
|
};
|
|
7742
|
-
function
|
|
8062
|
+
function parseProviderArgsSafely(command, args) {
|
|
8063
|
+
try {
|
|
8064
|
+
return parseProviderArgs(command, args);
|
|
8065
|
+
} catch (error) {
|
|
8066
|
+
if (error instanceof ProviderCliError) throw error;
|
|
8067
|
+
if (error instanceof Error && error.name === "ParseArgsError") throw new ProviderCliError("invalid_arguments", error.message);
|
|
8068
|
+
throw error;
|
|
8069
|
+
}
|
|
8070
|
+
}
|
|
8071
|
+
function parseProviderArgs(command, args) {
|
|
8072
|
+
const common = { root: { type: "string" } };
|
|
8073
|
+
switch (command) {
|
|
8074
|
+
case "list": {
|
|
8075
|
+
const { values, positionals } = parseArgs({
|
|
8076
|
+
args,
|
|
8077
|
+
options: {
|
|
8078
|
+
...common,
|
|
8079
|
+
json: { type: "boolean" }
|
|
8080
|
+
},
|
|
8081
|
+
allowPositionals: true,
|
|
8082
|
+
strict: true
|
|
8083
|
+
});
|
|
8084
|
+
requirePositionals(positionals, 0, "providers list");
|
|
8085
|
+
return {
|
|
8086
|
+
command,
|
|
8087
|
+
root: values.root,
|
|
8088
|
+
json: values.json ?? false
|
|
8089
|
+
};
|
|
8090
|
+
}
|
|
8091
|
+
case "set": {
|
|
8092
|
+
const { values, positionals } = parseArgs({
|
|
8093
|
+
args,
|
|
8094
|
+
options: {
|
|
8095
|
+
...common,
|
|
8096
|
+
"base-url": { type: "string" },
|
|
8097
|
+
api: { type: "string" },
|
|
8098
|
+
model: {
|
|
8099
|
+
type: "string",
|
|
8100
|
+
multiple: true
|
|
8101
|
+
},
|
|
8102
|
+
"clear-models": { type: "boolean" },
|
|
8103
|
+
"api-key-stdin": { type: "boolean" },
|
|
8104
|
+
"clear-api-key": { type: "boolean" }
|
|
8105
|
+
},
|
|
8106
|
+
allowPositionals: true,
|
|
8107
|
+
strict: true
|
|
8108
|
+
});
|
|
8109
|
+
requirePositionals(positionals, 1, "providers set <id>");
|
|
8110
|
+
if (values.model && values["clear-models"]) throw new ProviderCliError("invalid_arguments", "--model and --clear-models cannot be used together");
|
|
8111
|
+
if (values["api-key-stdin"] && values["clear-api-key"]) throw new ProviderCliError("invalid_arguments", "--api-key-stdin and --clear-api-key cannot be used together");
|
|
8112
|
+
return {
|
|
8113
|
+
command,
|
|
8114
|
+
root: values.root,
|
|
8115
|
+
providerId: positionals[0],
|
|
8116
|
+
baseUrl: values["base-url"],
|
|
8117
|
+
api: values.api,
|
|
8118
|
+
models: values["clear-models"] ? [] : values.model,
|
|
8119
|
+
apiKeyStdin: values["api-key-stdin"] ?? false,
|
|
8120
|
+
clearApiKey: values["clear-api-key"] ?? false
|
|
8121
|
+
};
|
|
8122
|
+
}
|
|
8123
|
+
case "discover": {
|
|
8124
|
+
const { values, positionals } = parseArgs({
|
|
8125
|
+
args,
|
|
8126
|
+
options: {
|
|
8127
|
+
...common,
|
|
8128
|
+
save: { type: "boolean" },
|
|
8129
|
+
json: { type: "boolean" }
|
|
8130
|
+
},
|
|
8131
|
+
allowPositionals: true,
|
|
8132
|
+
strict: true
|
|
8133
|
+
});
|
|
8134
|
+
requirePositionals(positionals, 1, "providers discover <id>");
|
|
8135
|
+
return {
|
|
8136
|
+
command,
|
|
8137
|
+
root: values.root,
|
|
8138
|
+
providerId: positionals[0],
|
|
8139
|
+
save: values.save ?? false,
|
|
8140
|
+
json: values.json ?? false
|
|
8141
|
+
};
|
|
8142
|
+
}
|
|
8143
|
+
case "remove":
|
|
8144
|
+
case "logout": {
|
|
8145
|
+
const { values, positionals } = parseArgs({
|
|
8146
|
+
args,
|
|
8147
|
+
options: {
|
|
8148
|
+
...common,
|
|
8149
|
+
yes: { type: "boolean" }
|
|
8150
|
+
},
|
|
8151
|
+
allowPositionals: true,
|
|
8152
|
+
strict: true
|
|
8153
|
+
});
|
|
8154
|
+
requirePositionals(positionals, 1, `providers ${command} <id>`);
|
|
8155
|
+
return {
|
|
8156
|
+
command,
|
|
8157
|
+
root: values.root,
|
|
8158
|
+
providerId: positionals[0],
|
|
8159
|
+
yes: values.yes ?? false
|
|
8160
|
+
};
|
|
8161
|
+
}
|
|
8162
|
+
case "login": {
|
|
8163
|
+
const { values, positionals } = parseArgs({
|
|
8164
|
+
args,
|
|
8165
|
+
options: {
|
|
8166
|
+
...common,
|
|
8167
|
+
"auth-method": { type: "string" }
|
|
8168
|
+
},
|
|
8169
|
+
allowPositionals: true,
|
|
8170
|
+
strict: true
|
|
8171
|
+
});
|
|
8172
|
+
requirePositionals(positionals, 1, "providers login <id>");
|
|
8173
|
+
return {
|
|
8174
|
+
command,
|
|
8175
|
+
root: values.root,
|
|
8176
|
+
providerId: positionals[0],
|
|
8177
|
+
authMethod: values["auth-method"]
|
|
8178
|
+
};
|
|
8179
|
+
}
|
|
8180
|
+
default: throw new ProviderCliError("invalid_arguments", `Unknown providers command "${String(command)}"`);
|
|
8181
|
+
}
|
|
8182
|
+
}
|
|
8183
|
+
async function createContext(rootFlag, dependencies, installInterruptHandler) {
|
|
8184
|
+
const store = new AgentServerStore(resolveAgentServerRoot({ root: rootFlag ?? dependencies.envRoot ?? loadAgentServerEnvConfig().root })).ensure();
|
|
8185
|
+
const secrets = new FileSecretProvider({
|
|
8186
|
+
root: store.secretsDir,
|
|
8187
|
+
writable: true
|
|
8188
|
+
});
|
|
8189
|
+
const secretProviders = createNodeSecretProviderRegistry().register(secrets);
|
|
8190
|
+
const stderr = dependencies.stderr ?? console.error;
|
|
8191
|
+
const logger = createCliLogger(stderr);
|
|
8192
|
+
const configuration = dependencies.configuration ?? new ProviderConfigurationService({
|
|
8193
|
+
store,
|
|
8194
|
+
secrets,
|
|
8195
|
+
secretProviders,
|
|
8196
|
+
logger
|
|
8197
|
+
});
|
|
8198
|
+
const oauth = dependencies.oauth ?? await OAuthProviderService.create({
|
|
8199
|
+
authPath: store.piAuthJsonPath,
|
|
8200
|
+
logger
|
|
8201
|
+
});
|
|
8202
|
+
const controller = new AbortController();
|
|
8203
|
+
const onInterrupt = () => controller.abort(/* @__PURE__ */ new Error("Interrupted"));
|
|
8204
|
+
if (installInterruptHandler) process.once("SIGINT", onInterrupt);
|
|
8205
|
+
const signal = dependencies.signal ? AbortSignal.any([dependencies.signal, controller.signal]) : controller.signal;
|
|
8206
|
+
const question = dependencies.question ?? defaultQuestion;
|
|
8207
|
+
const stdinIsTTY = dependencies.stdinIsTTY ?? dependencies.interactive ?? Boolean(process.stdin.isTTY);
|
|
8208
|
+
const stdoutIsTTY = dependencies.stdoutIsTTY ?? dependencies.interactive ?? Boolean(process.stdout.isTTY);
|
|
7743
8209
|
return {
|
|
7744
|
-
|
|
8210
|
+
configuration,
|
|
8211
|
+
oauth,
|
|
8212
|
+
stdout: dependencies.stdout ?? console.log,
|
|
8213
|
+
stderr,
|
|
8214
|
+
readStdin: () => raceWithSignal(dependencies.readStdin?.() ?? defaultReadStdin(signal), signal),
|
|
8215
|
+
question: (prompt) => question(prompt, signal),
|
|
8216
|
+
openUrl: async (url) => {
|
|
8217
|
+
if (dependencies.openUrl) await dependencies.openUrl(url);
|
|
8218
|
+
else await defaultOpenUrl(url);
|
|
8219
|
+
},
|
|
8220
|
+
interactive: dependencies.interactive ?? (stdinIsTTY && stdoutIsTTY),
|
|
8221
|
+
stdinIsTTY,
|
|
8222
|
+
signal,
|
|
8223
|
+
close: () => {
|
|
8224
|
+
if (installInterruptHandler) process.removeListener("SIGINT", onInterrupt);
|
|
8225
|
+
}
|
|
8226
|
+
};
|
|
8227
|
+
}
|
|
8228
|
+
function listProviders(context, json) {
|
|
8229
|
+
const configuredProviders = context.configuration.list();
|
|
8230
|
+
const oauthProviders = context.oauth.list();
|
|
8231
|
+
if (json) {
|
|
8232
|
+
context.stdout(JSON.stringify({
|
|
8233
|
+
configuredProviders,
|
|
8234
|
+
oauthProviders
|
|
8235
|
+
}));
|
|
8236
|
+
return 0;
|
|
8237
|
+
}
|
|
8238
|
+
const configured = Object.entries(configuredProviders);
|
|
8239
|
+
context.stdout("Configured providers:");
|
|
8240
|
+
if (configured.length === 0) context.stdout(" (none)");
|
|
8241
|
+
for (const [id, provider] of configured) context.stdout(` ${id} ${provider.baseUrl} ${provider.models.length} model(s)${provider.hasApiKey ? " API key configured" : ""}`);
|
|
8242
|
+
context.stdout("OAuth providers:");
|
|
8243
|
+
if (oauthProviders.length === 0) context.stdout(" (none)");
|
|
8244
|
+
for (const provider of oauthProviders) context.stdout(` ${provider.id} ${provider.name} ${provider.connected ? "connected" : "not connected"}`);
|
|
8245
|
+
return 0;
|
|
8246
|
+
}
|
|
8247
|
+
async function setProvider(context, parsed) {
|
|
8248
|
+
let apiKey;
|
|
8249
|
+
if (parsed.apiKeyStdin) {
|
|
8250
|
+
if (context.stdinIsTTY) throw new ProviderCliError("invalid_input", "--api-key-stdin requires redirected stdin; pipe the API key into this command");
|
|
8251
|
+
apiKey = (await context.readStdin()).replace(/[\r\n]+$/u, "");
|
|
8252
|
+
if (!apiKey) throw new ProviderCliError("invalid_input", "No API key was received on stdin");
|
|
8253
|
+
}
|
|
8254
|
+
const provider = await context.configuration.set(parsed.providerId, {
|
|
8255
|
+
...parsed.baseUrl ? { baseUrl: parsed.baseUrl } : {},
|
|
8256
|
+
...parsed.api ? { api: parsed.api } : {},
|
|
8257
|
+
...parsed.models ? { models: parsed.models } : {},
|
|
8258
|
+
...apiKey ? { apiKey } : {},
|
|
8259
|
+
...parsed.clearApiKey ? { clearApiKey: true } : {}
|
|
8260
|
+
}, { signal: context.signal });
|
|
8261
|
+
context.stdout(JSON.stringify({
|
|
8262
|
+
id: parsed.providerId,
|
|
8263
|
+
...provider
|
|
8264
|
+
}));
|
|
8265
|
+
return 0;
|
|
8266
|
+
}
|
|
8267
|
+
async function discoverProvider(context, parsed) {
|
|
8268
|
+
const result = await context.configuration.discover(parsed.providerId, {
|
|
8269
|
+
save: parsed.save,
|
|
8270
|
+
signal: context.signal
|
|
8271
|
+
});
|
|
8272
|
+
if (parsed.json) context.stdout(JSON.stringify(result));
|
|
8273
|
+
else for (const model of result.models) context.stdout(model);
|
|
8274
|
+
return 0;
|
|
8275
|
+
}
|
|
8276
|
+
async function removeProvider(context, parsed) {
|
|
8277
|
+
if (!await confirmDestructive(context, parsed.yes, `Remove configured provider "${parsed.providerId}"? [y/N] `)) {
|
|
8278
|
+
context.stderr("Provider removal cancelled.");
|
|
8279
|
+
return 1;
|
|
8280
|
+
}
|
|
8281
|
+
await context.configuration.remove(parsed.providerId, { signal: context.signal });
|
|
8282
|
+
context.stdout(`Removed configured provider "${parsed.providerId}".`);
|
|
8283
|
+
return 0;
|
|
8284
|
+
}
|
|
8285
|
+
async function loginProvider(context, parsed) {
|
|
8286
|
+
if (!context.interactive) throw new ProviderCliError("non_interactive", "Provider login requires an interactive terminal");
|
|
8287
|
+
await context.oauth.login(parsed.providerId, {
|
|
8288
|
+
signal: context.signal,
|
|
7745
8289
|
notify: (event) => {
|
|
7746
8290
|
switch (event.type) {
|
|
7747
|
-
case "auth_url":
|
|
7748
|
-
|
|
7749
|
-
|
|
7750
|
-
|
|
8291
|
+
case "auth_url":
|
|
8292
|
+
context.stderr(event.instructions ?? "Authorize in your browser:");
|
|
8293
|
+
context.stderr(event.url);
|
|
8294
|
+
context.openUrl(event.url).catch(() => {
|
|
8295
|
+
context.stderr("Could not open the browser automatically; open the authorization URL above manually.");
|
|
7751
8296
|
});
|
|
7752
8297
|
break;
|
|
7753
8298
|
case "device_code":
|
|
7754
|
-
|
|
7755
|
-
|
|
7756
|
-
verificationUri: event.verificationUri
|
|
7757
|
-
});
|
|
8299
|
+
context.stderr(`Open ${event.verificationUri}`);
|
|
8300
|
+
context.stderr(`Enter code: ${event.userCode}`);
|
|
7758
8301
|
break;
|
|
7759
8302
|
case "info":
|
|
7760
8303
|
case "progress":
|
|
7761
|
-
|
|
8304
|
+
context.stderr(event.message);
|
|
7762
8305
|
break;
|
|
7763
8306
|
}
|
|
7764
8307
|
},
|
|
7765
8308
|
prompt: async (prompt) => {
|
|
7766
|
-
if (prompt.type
|
|
7767
|
-
|
|
7768
|
-
|
|
7769
|
-
|
|
7770
|
-
|
|
7771
|
-
|
|
7772
|
-
})
|
|
7773
|
-
}
|
|
7774
|
-
|
|
7775
|
-
return selected;
|
|
8309
|
+
if (prompt.type === "select") {
|
|
8310
|
+
if (parsed.authMethod) {
|
|
8311
|
+
if (!prompt.options.some((option) => option.id === parsed.authMethod)) throw new ProviderCliError("unsupported_auth_method", `OAuth auth method "${parsed.authMethod}" is not available`);
|
|
8312
|
+
return parsed.authMethod;
|
|
8313
|
+
}
|
|
8314
|
+
context.stderr(prompt.message);
|
|
8315
|
+
for (const option of prompt.options) context.stderr(` ${option.id}: ${option.label}`);
|
|
8316
|
+
}
|
|
8317
|
+
return context.question(`${prompt.message} `);
|
|
7776
8318
|
}
|
|
7777
|
-
};
|
|
8319
|
+
}, { signal: context.signal });
|
|
8320
|
+
context.stdout(`Connected OAuth provider "${parsed.providerId}".`);
|
|
8321
|
+
return 0;
|
|
7778
8322
|
}
|
|
7779
|
-
function
|
|
7780
|
-
|
|
7781
|
-
|
|
7782
|
-
|
|
7783
|
-
});
|
|
7784
|
-
try {
|
|
7785
|
-
writeFileSync(authPath, "{}\n", {
|
|
7786
|
-
flag: "wx",
|
|
7787
|
-
mode: 384
|
|
7788
|
-
});
|
|
7789
|
-
} catch (error) {
|
|
7790
|
-
if (error.code !== "EEXIST") throw error;
|
|
8323
|
+
async function logoutProvider(context, parsed) {
|
|
8324
|
+
if (!await confirmDestructive(context, parsed.yes, `Log out OAuth provider "${parsed.providerId}"? [y/N] `)) {
|
|
8325
|
+
context.stderr("Provider logout cancelled.");
|
|
8326
|
+
return 1;
|
|
7791
8327
|
}
|
|
7792
|
-
|
|
7793
|
-
|
|
8328
|
+
await context.oauth.logout(parsed.providerId, context.signal);
|
|
8329
|
+
context.stdout(`Logged out OAuth provider "${parsed.providerId}".`);
|
|
8330
|
+
return 0;
|
|
8331
|
+
}
|
|
8332
|
+
async function confirmDestructive(context, yes, message) {
|
|
8333
|
+
if (yes) return true;
|
|
8334
|
+
if (!context.interactive) throw new ProviderCliError("non_interactive", "--yes is required outside an interactive terminal");
|
|
8335
|
+
return /^y(es)?$/iu.test((await context.question(message)).trim());
|
|
8336
|
+
}
|
|
8337
|
+
function requirePositionals(positionals, expected, usage) {
|
|
8338
|
+
if (positionals.length !== expected) throw new ProviderCliError("invalid_arguments", `Usage: ${usage}`);
|
|
8339
|
+
}
|
|
8340
|
+
function publicCliError(error, signal) {
|
|
8341
|
+
if (signal?.aborted) return "Provider operation interrupted.";
|
|
8342
|
+
if (error instanceof ProviderCliError || error instanceof ProviderConfigurationError || error instanceof OAuthProviderError || error instanceof ProviderLockError) return error.message;
|
|
8343
|
+
return "Provider operation failed.";
|
|
8344
|
+
}
|
|
8345
|
+
async function defaultReadStdin(signal) {
|
|
8346
|
+
let value = "";
|
|
8347
|
+
process.stdin.setEncoding("utf8");
|
|
8348
|
+
for await (const chunk of process.stdin) {
|
|
8349
|
+
if (signal.aborted) throw new Error("Interrupted");
|
|
8350
|
+
value += chunk;
|
|
8351
|
+
}
|
|
8352
|
+
return value;
|
|
8353
|
+
}
|
|
8354
|
+
function raceWithSignal(work, signal) {
|
|
8355
|
+
if (signal.aborted) return Promise.reject(/* @__PURE__ */ new Error("Interrupted"));
|
|
8356
|
+
return new Promise((resolve, reject) => {
|
|
8357
|
+
const onAbort = () => reject(/* @__PURE__ */ new Error("Interrupted"));
|
|
8358
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
8359
|
+
work.then(resolve, reject).finally(() => {
|
|
8360
|
+
signal.removeEventListener("abort", onAbort);
|
|
8361
|
+
});
|
|
8362
|
+
});
|
|
8363
|
+
}
|
|
8364
|
+
async function defaultQuestion(prompt, signal) {
|
|
8365
|
+
const readline = createInterface({
|
|
8366
|
+
input: process.stdin,
|
|
8367
|
+
output: process.stdout
|
|
8368
|
+
});
|
|
7794
8369
|
try {
|
|
7795
|
-
|
|
7796
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`Pi credential store is not an object: ${authPath}`);
|
|
7797
|
-
const credentials = parsed;
|
|
7798
|
-
if (credential) credentials[providerId] = credential;
|
|
7799
|
-
else delete credentials[providerId];
|
|
7800
|
-
writeFileSync(temp, `${JSON.stringify(credentials, null, 2)}\n`, { mode: 384 });
|
|
7801
|
-
renameSync(temp, authPath);
|
|
8370
|
+
return await readline.question(prompt, { signal });
|
|
7802
8371
|
} finally {
|
|
7803
|
-
|
|
7804
|
-
release();
|
|
8372
|
+
readline.close();
|
|
7805
8373
|
}
|
|
7806
8374
|
}
|
|
7807
|
-
function
|
|
8375
|
+
function browserLaunchCommand(platform, url) {
|
|
8376
|
+
if (platform === "darwin") return {
|
|
8377
|
+
executable: "open",
|
|
8378
|
+
args: [url]
|
|
8379
|
+
};
|
|
8380
|
+
if (platform === "win32") return {
|
|
8381
|
+
executable: "explorer.exe",
|
|
8382
|
+
args: [url]
|
|
8383
|
+
};
|
|
7808
8384
|
return {
|
|
7809
|
-
|
|
7810
|
-
|
|
7811
|
-
if (info.instructions) login.instructions = info.instructions;
|
|
7812
|
-
logger.info({
|
|
7813
|
-
event: "agent-server.subscription_login_transition",
|
|
7814
|
-
operationId: login.operationId,
|
|
7815
|
-
providerId: login.providerId,
|
|
7816
|
-
transition: "authorization_ready"
|
|
7817
|
-
}, "Subscription authorization URL ready");
|
|
7818
|
-
login.infoArrived();
|
|
7819
|
-
},
|
|
7820
|
-
onDeviceCode: (info) => {
|
|
7821
|
-
login.userCode = info.userCode;
|
|
7822
|
-
login.verificationUri = info.verificationUri;
|
|
7823
|
-
logger.info({
|
|
7824
|
-
event: "agent-server.subscription_login_transition",
|
|
7825
|
-
operationId: login.operationId,
|
|
7826
|
-
providerId: login.providerId,
|
|
7827
|
-
transition: "device_code_ready"
|
|
7828
|
-
}, "Subscription device code ready");
|
|
7829
|
-
login.infoArrived();
|
|
7830
|
-
},
|
|
7831
|
-
onPrompt: () => Promise.reject(new AgentServerSubscriptionError("login_unsupported_prompt", "This provider flow needs an interactive prompt; run `pi /login` in a terminal instead")),
|
|
7832
|
-
onSelect: (prompt) => Promise.resolve((prompt.options.find((option) => /device/i.test(option.id)) ?? prompt.options[0])?.id),
|
|
7833
|
-
signal: login.abort.signal
|
|
8385
|
+
executable: "xdg-open",
|
|
8386
|
+
args: [url]
|
|
7834
8387
|
};
|
|
7835
8388
|
}
|
|
7836
|
-
function
|
|
7837
|
-
|
|
7838
|
-
return
|
|
8389
|
+
function defaultOpenUrl(url, spawnProcess = spawn) {
|
|
8390
|
+
const { executable, args } = browserLaunchCommand(process.platform, url);
|
|
8391
|
+
return new Promise((resolve, reject) => {
|
|
8392
|
+
const child = spawnProcess(executable, args, {
|
|
8393
|
+
detached: true,
|
|
8394
|
+
stdio: "ignore"
|
|
8395
|
+
});
|
|
8396
|
+
child.once("error", reject);
|
|
8397
|
+
child.once("spawn", () => {
|
|
8398
|
+
child.unref();
|
|
8399
|
+
resolve();
|
|
8400
|
+
});
|
|
8401
|
+
});
|
|
7839
8402
|
}
|
|
7840
|
-
function
|
|
7841
|
-
const
|
|
7842
|
-
|
|
7843
|
-
|
|
7844
|
-
|
|
8403
|
+
function createCliLogger(stderr) {
|
|
8404
|
+
const write = (level, context, message) => {
|
|
8405
|
+
const safeContext = Object.fromEntries(Object.entries(context).filter(([key, value]) => !/(?:credential|message|secret|token|url)/iu.test(key) && [
|
|
8406
|
+
"string",
|
|
8407
|
+
"number",
|
|
8408
|
+
"boolean"
|
|
8409
|
+
].includes(typeof value)));
|
|
8410
|
+
stderr(JSON.stringify({
|
|
8411
|
+
level,
|
|
8412
|
+
message,
|
|
8413
|
+
...safeContext
|
|
8414
|
+
}));
|
|
8415
|
+
};
|
|
8416
|
+
return {
|
|
8417
|
+
info: (context, message) => write("info", context, message),
|
|
8418
|
+
warn: (context, message) => write("warn", context, message)
|
|
8419
|
+
};
|
|
8420
|
+
}
|
|
8421
|
+
//#endregion
|
|
8422
|
+
//#region ../../libs/loopback-companion/src/errors.ts
|
|
8423
|
+
var LoopbackViolationError = class extends Error {
|
|
8424
|
+
name = "LoopbackViolationError";
|
|
8425
|
+
constructor(kind, message, options) {
|
|
8426
|
+
super(message, options);
|
|
8427
|
+
this.kind = kind;
|
|
8428
|
+
}
|
|
8429
|
+
};
|
|
8430
|
+
function isLoopbackViolation(error) {
|
|
8431
|
+
return error instanceof LoopbackViolationError;
|
|
8432
|
+
}
|
|
8433
|
+
//#endregion
|
|
8434
|
+
//#region ../../libs/loopback-companion/src/origin.ts
|
|
8435
|
+
/** Hostnames accepted as loopback for companion servers and origins. */
|
|
8436
|
+
function isLoopbackHostname(hostname) {
|
|
8437
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
|
|
8438
|
+
}
|
|
8439
|
+
/**
|
|
8440
|
+
* Normalize and validate a browser origin. Accepts exact `https:` origins,
|
|
8441
|
+
* or `http:` origins whose host is loopback. Rejects values that carry a
|
|
8442
|
+
* path, trailing slash, credentials, or any other non-origin decoration
|
|
8443
|
+
* (`url.origin !== value` catches all of those).
|
|
8444
|
+
*/
|
|
8445
|
+
function normalizeOrigin(value) {
|
|
8446
|
+
let url;
|
|
8447
|
+
try {
|
|
8448
|
+
url = new URL(value);
|
|
8449
|
+
} catch (cause) {
|
|
8450
|
+
throw new LoopbackViolationError("origin_invalid", "Origin is not valid", { cause });
|
|
8451
|
+
}
|
|
8452
|
+
if (url.origin !== value || url.protocol !== "https:" && !(url.protocol === "http:" && isLoopbackHostname(url.hostname))) throw new LoopbackViolationError("origin_invalid", "Origin is not valid");
|
|
8453
|
+
return url.origin;
|
|
8454
|
+
}
|
|
8455
|
+
/** Parse a comma-separated origin list (config format shared by companions). */
|
|
8456
|
+
function parseAllowedOrigins(csv) {
|
|
8457
|
+
return csv.split(",").map((origin) => origin.trim()).filter(Boolean);
|
|
8458
|
+
}
|
|
8459
|
+
/**
|
|
8460
|
+
* Exact-origin allowlist. Every configured origin is normalized eagerly so a
|
|
8461
|
+
* misconfigured allowlist fails at startup, not at request time.
|
|
8462
|
+
*/
|
|
8463
|
+
var OriginAllowlist = class {
|
|
8464
|
+
origins;
|
|
8465
|
+
constructor(allowedOrigins) {
|
|
8466
|
+
if (allowedOrigins.length === 0) throw new Error("OriginAllowlist requires at least one origin");
|
|
8467
|
+
this.origins = new Set(allowedOrigins.map((origin) => normalizeOrigin(origin)));
|
|
8468
|
+
}
|
|
8469
|
+
has(origin) {
|
|
8470
|
+
try {
|
|
8471
|
+
return this.origins.has(normalizeOrigin(origin));
|
|
8472
|
+
} catch {
|
|
8473
|
+
return false;
|
|
8474
|
+
}
|
|
8475
|
+
}
|
|
8476
|
+
/** Return the normalized origin or throw `origin_not_allowed`. */
|
|
8477
|
+
assert(value) {
|
|
8478
|
+
let origin;
|
|
8479
|
+
try {
|
|
8480
|
+
origin = normalizeOrigin(value);
|
|
8481
|
+
} catch {
|
|
8482
|
+
throw new LoopbackViolationError("origin_not_allowed", "Origin is not allowed");
|
|
8483
|
+
}
|
|
8484
|
+
if (!this.origins.has(origin)) throw new LoopbackViolationError("origin_not_allowed", "Origin is not allowed");
|
|
8485
|
+
return origin;
|
|
8486
|
+
}
|
|
8487
|
+
};
|
|
8488
|
+
/** Extract and require the `Origin` header value. */
|
|
8489
|
+
function requireOriginHeader(headers) {
|
|
8490
|
+
const origin = headers.origin;
|
|
8491
|
+
if (typeof origin !== "string" || origin.length === 0) throw new LoopbackViolationError("origin_required", "Origin is required");
|
|
8492
|
+
return origin;
|
|
8493
|
+
}
|
|
8494
|
+
//#endregion
|
|
8495
|
+
//#region ../../libs/loopback-companion/src/fastify.ts
|
|
8496
|
+
var CORS_PREFLIGHT_MAX_AGE_SECONDS = 600;
|
|
8497
|
+
var PRIVATE_NETWORK_REQUEST_HEADER = "access-control-request-private-network";
|
|
8498
|
+
var PRIVATE_NETWORK_ALLOW_HEADER = "access-control-allow-private-network";
|
|
8499
|
+
/**
|
|
8500
|
+
* Enforce that the `Host` header identifies loopback. Blocks DNS-rebinding
|
|
8501
|
+
* setups where a public hostname resolves to 127.0.0.1: the browser then
|
|
8502
|
+
* sends that hostname as `Host`, and the request is refused here even
|
|
8503
|
+
* though the socket is loopback.
|
|
8504
|
+
*/
|
|
8505
|
+
function requireLoopbackHost(request) {
|
|
8506
|
+
const host = request.headers.host;
|
|
8507
|
+
if (!host) throw new LoopbackViolationError("host_required", "Host header is required");
|
|
8508
|
+
let hostname;
|
|
8509
|
+
try {
|
|
8510
|
+
hostname = new URL(`http://${host}`).hostname;
|
|
8511
|
+
} catch {
|
|
8512
|
+
throw new LoopbackViolationError("host_not_loopback", "Host header must identify loopback");
|
|
8513
|
+
}
|
|
8514
|
+
if (!isLoopbackHostname(hostname === "::1" ? "[::1]" : hostname)) throw new LoopbackViolationError("host_not_loopback", "Host header must identify loopback");
|
|
8515
|
+
}
|
|
8516
|
+
/**
|
|
8517
|
+
* Register the loopback-companion security profile on a Fastify app:
|
|
8518
|
+
*
|
|
8519
|
+
* - loopback `Host` enforcement on every request;
|
|
8520
|
+
* - `cache-control: no-store` on every response;
|
|
8521
|
+
* - strict UTF-8 JSON body parsing (invalid bodies raise a typed violation);
|
|
8522
|
+
* - exact-origin CORS (opaque/`null` origins get no CORS response but are
|
|
8523
|
+
* not rejected here — route-level controls stay mandatory);
|
|
8524
|
+
* - hardened helmet defaults.
|
|
8525
|
+
*
|
|
8526
|
+
*/
|
|
8527
|
+
function registerLoopbackSecurity(app, options) {
|
|
8528
|
+
if (!options.allowedOrigins && !options.isOriginAllowed) throw new Error("registerLoopbackSecurity requires allowedOrigins or isOriginAllowed");
|
|
8529
|
+
const primaryAllowlist = options.allowedOrigins ? new OriginAllowlist(options.allowedOrigins) : null;
|
|
8530
|
+
const selfAllowlist = options.selfOrigins && options.selfOrigins.length > 0 ? new OriginAllowlist(options.selfOrigins) : null;
|
|
8531
|
+
const isOriginAllowed = (origin) => selfAllowlist?.has(origin) === true || primaryAllowlist?.has(origin) === true || options.isOriginAllowed?.(origin) === true;
|
|
8532
|
+
app.addHook("onRequest", (request, _reply, done) => {
|
|
8533
|
+
requireLoopbackHost(request);
|
|
8534
|
+
done();
|
|
8535
|
+
});
|
|
8536
|
+
app.addHook("onSend", async (request, reply, payload) => {
|
|
8537
|
+
reply.header("cache-control", "no-store");
|
|
8538
|
+
if (request.method === "OPTIONS" && request.headers[PRIVATE_NETWORK_REQUEST_HEADER] === "true" && reply.getHeader("access-control-allow-origin") === request.headers.origin) reply.header(PRIVATE_NETWORK_ALLOW_HEADER, "true");
|
|
8539
|
+
return payload;
|
|
8540
|
+
});
|
|
8541
|
+
app.removeContentTypeParser("application/json");
|
|
8542
|
+
app.addContentTypeParser("application/json", { parseAs: "buffer" }, (_request, body, done) => {
|
|
8543
|
+
try {
|
|
8544
|
+
const json = new TextDecoder("utf-8", { fatal: true }).decode(typeof body === "string" ? Buffer.from(body) : body);
|
|
8545
|
+
done(null, JSON.parse(json));
|
|
8546
|
+
} catch (cause) {
|
|
8547
|
+
done(new LoopbackViolationError("body_not_utf8_json", "Request body must be valid UTF-8 JSON", { cause }), void 0);
|
|
8548
|
+
}
|
|
8549
|
+
});
|
|
8550
|
+
app.register(cors, {
|
|
8551
|
+
allowedHeaders: ["content-type", ...options.allowedHeaders ?? []],
|
|
8552
|
+
maxAge: CORS_PREFLIGHT_MAX_AGE_SECONDS,
|
|
8553
|
+
methods: [...options.methods ?? [
|
|
8554
|
+
"GET",
|
|
8555
|
+
"POST",
|
|
8556
|
+
"OPTIONS"
|
|
8557
|
+
]],
|
|
8558
|
+
origin: (origin, callback) => {
|
|
8559
|
+
if (!origin || origin === "null") {
|
|
8560
|
+
callback(null, false);
|
|
8561
|
+
return;
|
|
8562
|
+
}
|
|
8563
|
+
if (isOriginAllowed(origin)) {
|
|
8564
|
+
callback(null, true);
|
|
8565
|
+
return;
|
|
8566
|
+
}
|
|
8567
|
+
callback(new LoopbackViolationError("origin_not_allowed", "Origin is not allowed"), false);
|
|
8568
|
+
}
|
|
8569
|
+
});
|
|
8570
|
+
app.register(helmet, {
|
|
8571
|
+
contentSecurityPolicy: {
|
|
8572
|
+
useDefaults: false,
|
|
8573
|
+
directives: options.contentSecurityPolicyDirectives ?? {
|
|
8574
|
+
baseUri: ["'none'"],
|
|
8575
|
+
defaultSrc: ["'none'"],
|
|
8576
|
+
formAction: ["'self'"],
|
|
8577
|
+
frameAncestors: ["'none'"],
|
|
8578
|
+
styleSrc: ["'unsafe-inline'"]
|
|
8579
|
+
}
|
|
8580
|
+
},
|
|
8581
|
+
crossOriginEmbedderPolicy: false,
|
|
8582
|
+
crossOriginOpenerPolicy: false,
|
|
8583
|
+
crossOriginResourcePolicy: { policy: "same-origin" },
|
|
8584
|
+
hsts: false,
|
|
8585
|
+
referrerPolicy: { policy: "no-referrer" }
|
|
8586
|
+
});
|
|
7845
8587
|
}
|
|
7846
|
-
|
|
7847
|
-
|
|
7848
|
-
|
|
7849
|
-
|
|
7850
|
-
|
|
7851
|
-
...authUrl ? { authUrl } : {},
|
|
7852
|
-
...instructions ? { instructions } : {},
|
|
7853
|
-
...userCode ? { userCode } : {},
|
|
7854
|
-
...verificationUri ? { verificationUri } : {},
|
|
7855
|
-
...error ? { error } : {},
|
|
7856
|
-
...waitingForAuthorization ? { waitingForAuthorization } : {}
|
|
7857
|
-
};
|
|
8588
|
+
//#endregion
|
|
8589
|
+
//#region ../../libs/loopback-companion/src/fetch-metadata.ts
|
|
8590
|
+
function headerValue(headers, name) {
|
|
8591
|
+
const value = headers[name];
|
|
8592
|
+
return typeof value === "string" ? value : void 0;
|
|
7858
8593
|
}
|
|
7859
|
-
|
|
7860
|
-
|
|
7861
|
-
|
|
7862
|
-
|
|
7863
|
-
|
|
7864
|
-
|
|
7865
|
-
|
|
7866
|
-
|
|
8594
|
+
/**
|
|
8595
|
+
* Require that a request is a top-level browser navigation (used by local
|
|
8596
|
+
* approval pages that must be opened as a document, never fetched).
|
|
8597
|
+
*/
|
|
8598
|
+
function assertNavigationRequest(headers) {
|
|
8599
|
+
const site = headerValue(headers, "sec-fetch-site");
|
|
8600
|
+
const mode = headerValue(headers, "sec-fetch-mode");
|
|
8601
|
+
const destination = headerValue(headers, "sec-fetch-dest");
|
|
8602
|
+
if (site !== "cross-site" && site !== "same-origin" && site !== "none" || mode !== "navigate" || destination !== "document") throw new LoopbackViolationError("navigation_required", "Request must be opened as a browser navigation");
|
|
7867
8603
|
}
|
|
7868
|
-
|
|
7869
|
-
|
|
7870
|
-
|
|
8604
|
+
/**
|
|
8605
|
+
* Reject an explicit cross-site Fetch-Metadata signal. Only the explicit
|
|
8606
|
+
* `cross-site` value is rejected: Safari may omit Fetch Metadata on
|
|
8607
|
+
* same-origin form submissions, so the absence of the header is not treated
|
|
8608
|
+
* as a violation — callers keep their one-time token as the primary control.
|
|
8609
|
+
*/
|
|
8610
|
+
function rejectExplicitCrossSite(headers) {
|
|
8611
|
+
if (headerValue(headers, "sec-fetch-site") === "cross-site") throw new LoopbackViolationError("cross_site_rejected", "Request must not originate cross-site");
|
|
7871
8612
|
}
|
|
7872
|
-
|
|
7873
|
-
|
|
8613
|
+
//#endregion
|
|
8614
|
+
//#region src/lib/agent-server/lock.ts
|
|
8615
|
+
var AgentServerLockError = class extends Error {
|
|
8616
|
+
name = "AgentServerLockError";
|
|
7874
8617
|
constructor(code, message, options) {
|
|
7875
8618
|
super(message, options);
|
|
7876
8619
|
this.code = code;
|
|
7877
8620
|
}
|
|
7878
8621
|
};
|
|
7879
8622
|
/**
|
|
7880
|
-
* `
|
|
7881
|
-
*
|
|
7882
|
-
*
|
|
7883
|
-
* and @moltnet/agent-config's getConfigDir both resolve `~/.config/moltnet`,
|
|
7884
|
-
* so honouring XDG here gave one application two config roots: on a machine
|
|
7885
|
-
* with the variable set, the daemon wrote identities the CLI and SDK could not
|
|
7886
|
-
* read. `MOLTNET_AGENT_SERVER_ROOT` remains the explicit escape hatch for a
|
|
7887
|
-
* genuinely custom location.
|
|
8623
|
+
* Acquire the per-root singleton lock. `proper-lockfile` uses an atomic lock
|
|
8624
|
+
* directory at exactly `<root>/agent-server.lock`, recovers stale owners, and keeps
|
|
8625
|
+
* the mtime fresh while the supervisor is alive.
|
|
7888
8626
|
*/
|
|
7889
|
-
function
|
|
7890
|
-
const
|
|
7891
|
-
|
|
7892
|
-
return getConfigDir();
|
|
7893
|
-
}
|
|
7894
|
-
function providerEnvName(providerId) {
|
|
7895
|
-
return `MOLTNET_PROVIDER_${assertProviderId(providerId).replaceAll("-", "_").toUpperCase()}_API_KEY`;
|
|
7896
|
-
}
|
|
7897
|
-
function assertProviderEnvName(providerId, value) {
|
|
7898
|
-
const expected = providerEnvName(providerId);
|
|
7899
|
-
if (value !== expected) throw new AgentServerStoreError("invalid_state", `provider envName must be ${expected}`);
|
|
7900
|
-
return value;
|
|
7901
|
-
}
|
|
7902
|
-
function readJson(path) {
|
|
7903
|
-
let raw;
|
|
8627
|
+
async function acquireAgentServerLock(root, options = {}) {
|
|
8628
|
+
const path = join(root, "agent-server.lock");
|
|
8629
|
+
let releaseLock;
|
|
7904
8630
|
try {
|
|
7905
|
-
|
|
8631
|
+
releaseLock = await lock(root, {
|
|
8632
|
+
lockfilePath: path,
|
|
8633
|
+
realpath: false,
|
|
8634
|
+
retries: 0,
|
|
8635
|
+
...options.staleMs === void 0 ? {} : { stale: options.staleMs },
|
|
8636
|
+
...options.updateMs === void 0 ? {} : { update: options.updateMs },
|
|
8637
|
+
onCompromised: (cause) => {
|
|
8638
|
+
const error = new AgentServerLockError("compromised", `Agent Server lock ${path} was compromised: ${cause.message}`, { cause });
|
|
8639
|
+
if (options.onCompromised) {
|
|
8640
|
+
options.onCompromised(error);
|
|
8641
|
+
return;
|
|
8642
|
+
}
|
|
8643
|
+
throw error;
|
|
8644
|
+
}
|
|
8645
|
+
});
|
|
7906
8646
|
} catch (cause) {
|
|
7907
|
-
if (cause.code === "
|
|
7908
|
-
throw new
|
|
8647
|
+
if (cause.code === "ELOCKED") throw new AgentServerLockError("held", `another moltnet-agent server process already owns ${path}`, { cause });
|
|
8648
|
+
throw new AgentServerLockError("failed", `could not acquire Agent Server lock ${path}: ${cause.message}`, { cause });
|
|
7909
8649
|
}
|
|
8650
|
+
let released = false;
|
|
8651
|
+
return {
|
|
8652
|
+
path,
|
|
8653
|
+
async release() {
|
|
8654
|
+
if (released) return;
|
|
8655
|
+
released = true;
|
|
8656
|
+
await releaseLock();
|
|
8657
|
+
}
|
|
8658
|
+
};
|
|
8659
|
+
}
|
|
8660
|
+
/** Always release after normal completion or startup/runtime failure. */
|
|
8661
|
+
async function withAgentServerLock(root, work, options) {
|
|
8662
|
+
const held = await acquireAgentServerLock(root, options);
|
|
7910
8663
|
try {
|
|
7911
|
-
return
|
|
7912
|
-
}
|
|
7913
|
-
|
|
8664
|
+
return await work();
|
|
8665
|
+
} finally {
|
|
8666
|
+
await held.release();
|
|
7914
8667
|
}
|
|
7915
8668
|
}
|
|
7916
|
-
|
|
7917
|
-
|
|
7918
|
-
|
|
7919
|
-
|
|
7920
|
-
|
|
7921
|
-
|
|
7922
|
-
|
|
7923
|
-
|
|
7924
|
-
|
|
7925
|
-
|
|
8669
|
+
//#endregion
|
|
8670
|
+
//#region src/lib/agent-server/pairing.ts
|
|
8671
|
+
/**
|
|
8672
|
+
* One-click pairing ceremony for the agent server, mirroring the signer's
|
|
8673
|
+
* session/ceremony pattern (#2062 design):
|
|
8674
|
+
*
|
|
8675
|
+
* 1. Console (allowed origin) POSTs `/v1/pairings` → pending pairing bound
|
|
8676
|
+
* to that origin, with a one-time confirmation token.
|
|
8677
|
+
* 2. Console opens `http://127.0.0.1:<port>/pairings/<id>` in a new tab —
|
|
8678
|
+
* a navigation-gated local approval page naming the origin.
|
|
8679
|
+
* 3. One click POSTs the confirmation form (explicit cross-site rejected;
|
|
8680
|
+
* the one-time token is the primary CSRF control).
|
|
8681
|
+
* 4. Console claims `/v1/pairings/<id>/claim` from the same origin and
|
|
8682
|
+
* receives the bearer token exactly once; only its SHA-256 remains in
|
|
8683
|
+
* this supervisor process.
|
|
8684
|
+
*
|
|
8685
|
+
* The token exists for shared-machine cross-user protection and to bind
|
|
8686
|
+
* "this console session is the operator" — browser-vs-browser isolation is
|
|
8687
|
+
* already covered by the loopback-companion origin checks. Grants are
|
|
8688
|
+
* deliberately process-scoped: after the listening socket changes owners, a
|
|
8689
|
+
* token disclosed to an impostor on that port cannot authenticate to a later
|
|
8690
|
+
* supervisor process.
|
|
8691
|
+
*/
|
|
8692
|
+
var PENDING_TTL_MS = 600 * 1e3;
|
|
8693
|
+
var AgentServerPairingError = class extends Error {
|
|
8694
|
+
name = "AgentServerPairingError";
|
|
8695
|
+
constructor(code, message) {
|
|
8696
|
+
super(message);
|
|
8697
|
+
this.code = code;
|
|
7926
8698
|
}
|
|
8699
|
+
};
|
|
8700
|
+
function sha256Hex(value) {
|
|
8701
|
+
return createHash("sha256").update(value, "utf8").digest("hex");
|
|
7927
8702
|
}
|
|
7928
|
-
|
|
7929
|
-
|
|
7930
|
-
|
|
7931
|
-
|
|
7932
|
-
|
|
7933
|
-
|
|
7934
|
-
|
|
7935
|
-
|
|
7936
|
-
|
|
7937
|
-
this.
|
|
7938
|
-
this.runsDir = join(root, "runs");
|
|
7939
|
-
this.secretsDir = join(root, "secrets");
|
|
7940
|
-
this.piDir = join(root, "pi");
|
|
8703
|
+
function safeEqual(a, b) {
|
|
8704
|
+
const left = Buffer.from(a, "utf8");
|
|
8705
|
+
const right = Buffer.from(b, "utf8");
|
|
8706
|
+
return left.length === right.length && timingSafeEqual(left, right);
|
|
8707
|
+
}
|
|
8708
|
+
var PairingService = class {
|
|
8709
|
+
pending = /* @__PURE__ */ new Map();
|
|
8710
|
+
paired = /* @__PURE__ */ new Map();
|
|
8711
|
+
constructor(options = {}) {
|
|
8712
|
+
this.options = options;
|
|
7941
8713
|
}
|
|
7942
|
-
|
|
7943
|
-
return
|
|
8714
|
+
now() {
|
|
8715
|
+
return this.options.now?.() ?? Date.now();
|
|
7944
8716
|
}
|
|
7945
|
-
|
|
7946
|
-
|
|
7947
|
-
for (const dir of [
|
|
7948
|
-
this.root,
|
|
7949
|
-
this.identitiesDir,
|
|
7950
|
-
this.runsDir,
|
|
7951
|
-
this.secretsDir
|
|
7952
|
-
]) mkdirSync(dir, {
|
|
7953
|
-
recursive: true,
|
|
7954
|
-
mode: 448
|
|
7955
|
-
});
|
|
7956
|
-
return this;
|
|
8717
|
+
token() {
|
|
8718
|
+
return this.options.randomToken?.() ?? randomBytes(32).toString("base64url");
|
|
7957
8719
|
}
|
|
7958
|
-
|
|
7959
|
-
|
|
8720
|
+
sweep() {
|
|
8721
|
+
const now = this.now();
|
|
8722
|
+
for (const [id, pairing] of this.pending) if (pairing.expiresAt <= now) this.pending.delete(id);
|
|
7960
8723
|
}
|
|
7961
|
-
|
|
7962
|
-
|
|
7963
|
-
|
|
7964
|
-
|
|
7965
|
-
|
|
7966
|
-
|
|
8724
|
+
start(origin) {
|
|
8725
|
+
this.sweep();
|
|
8726
|
+
const pairingId = randomBytes(12).toString("hex");
|
|
8727
|
+
this.pending.set(pairingId, {
|
|
8728
|
+
origin,
|
|
8729
|
+
confirmToken: this.token(),
|
|
8730
|
+
expiresAt: this.now() + PENDING_TTL_MS,
|
|
8731
|
+
approved: false,
|
|
8732
|
+
bearerToken: null
|
|
8733
|
+
});
|
|
8734
|
+
return {
|
|
8735
|
+
pairingId,
|
|
8736
|
+
approvalPath: `/pairings/${pairingId}`
|
|
7967
8737
|
};
|
|
7968
|
-
|
|
7969
|
-
|
|
7970
|
-
|
|
7971
|
-
|
|
7972
|
-
|
|
7973
|
-
assertStoreName("agent name", alias);
|
|
7974
|
-
if (!isRecord$1(registration) || typeof registration.apiUrl !== "string" || registration.apiUrl.length === 0 || typeof registration.createdAt !== "string" || registration.createdAt.length === 0) throw new AgentServerStoreError("invalid_state", `pending registration "${alias}" is not valid`);
|
|
7975
|
-
}
|
|
8738
|
+
}
|
|
8739
|
+
/** Data for the local approval page. */
|
|
8740
|
+
approval(pairingId) {
|
|
8741
|
+
const pairing = this.require(pairingId);
|
|
8742
|
+
if (pairing.approved) throw new AgentServerPairingError("pairing_invalid", "Pairing is already approved");
|
|
7976
8743
|
return {
|
|
7977
|
-
|
|
7978
|
-
|
|
7979
|
-
activations: state.activations
|
|
8744
|
+
origin: pairing.origin,
|
|
8745
|
+
confirmToken: pairing.confirmToken
|
|
7980
8746
|
};
|
|
7981
8747
|
}
|
|
7982
|
-
|
|
7983
|
-
|
|
7984
|
-
|
|
7985
|
-
|
|
7986
|
-
|
|
7987
|
-
|
|
7988
|
-
/** Central identity directory, shared with the Go CLI layout. */
|
|
7989
|
-
identityDir(name) {
|
|
7990
|
-
return storeChildPath(this.identitiesDir, "identity alias", name);
|
|
8748
|
+
confirm(pairingId, confirmToken) {
|
|
8749
|
+
const pairing = this.require(pairingId);
|
|
8750
|
+
if (pairing.approved || !safeEqual(pairing.confirmToken, confirmToken)) throw new AgentServerPairingError("pairing_invalid", "Confirmation token is not valid");
|
|
8751
|
+
pairing.approved = true;
|
|
8752
|
+
pairing.bearerToken = this.token();
|
|
8753
|
+
return { origin: pairing.origin };
|
|
7991
8754
|
}
|
|
7992
|
-
|
|
7993
|
-
|
|
8755
|
+
claim(pairingId, origin) {
|
|
8756
|
+
const pairing = this.require(pairingId);
|
|
8757
|
+
if (pairing.origin !== origin) throw new AgentServerPairingError("pairing_origin_mismatch", "Pairing belongs to a different origin");
|
|
8758
|
+
if (!pairing.approved || !pairing.bearerToken) throw new AgentServerPairingError("pairing_not_approved", "Pairing has not been approved yet");
|
|
8759
|
+
const token = pairing.bearerToken;
|
|
8760
|
+
this.pending.delete(pairingId);
|
|
8761
|
+
this.paired.set(origin, sha256Hex(token));
|
|
8762
|
+
return { token };
|
|
7994
8763
|
}
|
|
7995
|
-
|
|
7996
|
-
const
|
|
7997
|
-
if (!
|
|
7998
|
-
if (!isRecord$1(selector) || selector.version !== 1 || selector.default_identity !== void 0 && typeof selector.default_identity !== "string") throw new AgentServerStoreError("invalid_state", "identity-selector.json is not a supported selector document");
|
|
7999
|
-
if (selector.default_identity) assertStoreName("identity alias", selector.default_identity);
|
|
8000
|
-
return selector;
|
|
8764
|
+
verify(origin, token) {
|
|
8765
|
+
const tokenHash = this.paired.get(origin);
|
|
8766
|
+
if (!tokenHash || !safeEqual(tokenHash, sha256Hex(token))) throw new AgentServerPairingError("pairing_token_invalid", "Pairing token is not valid for this origin");
|
|
8001
8767
|
}
|
|
8002
|
-
|
|
8003
|
-
|
|
8004
|
-
|
|
8005
|
-
|
|
8006
|
-
|
|
8768
|
+
require(pairingId) {
|
|
8769
|
+
this.sweep();
|
|
8770
|
+
const pairing = this.pending.get(pairingId);
|
|
8771
|
+
if (!pairing) throw new AgentServerPairingError("pairing_not_found", "Pairing was not found or has expired");
|
|
8772
|
+
return pairing;
|
|
8007
8773
|
}
|
|
8008
|
-
|
|
8009
|
-
|
|
8010
|
-
|
|
8011
|
-
|
|
8774
|
+
};
|
|
8775
|
+
function escapeHtml(value) {
|
|
8776
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """);
|
|
8777
|
+
}
|
|
8778
|
+
/** Minimal, dependency-free local approval page. */
|
|
8779
|
+
function renderPairingApprovalPage(input) {
|
|
8780
|
+
return `<!doctype html>
|
|
8781
|
+
<html lang="en">
|
|
8782
|
+
<head>
|
|
8783
|
+
<meta charset="utf-8" />
|
|
8784
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
8785
|
+
<title>MoltNet Agent — approve connection</title>
|
|
8786
|
+
<style>
|
|
8787
|
+
:root { color-scheme: light dark; }
|
|
8788
|
+
body { margin: 0; font: 16px/1.5 system-ui, sans-serif; display: grid; place-items: center; min-height: 100vh; background: Canvas; color: CanvasText; }
|
|
8789
|
+
main { max-width: 26rem; padding: 2rem; border: 1px solid color-mix(in srgb, CanvasText 20%, transparent); border-radius: 12px; }
|
|
8790
|
+
h1 { font-size: 1.2rem; margin: 0 0 0.5rem; }
|
|
8791
|
+
code { font-size: 0.95em; word-break: break-all; }
|
|
8792
|
+
button { margin-top: 1.25rem; font: inherit; padding: 0.6rem 1.4rem; border-radius: 8px; border: 1px solid color-mix(in srgb, CanvasText 30%, transparent); cursor: pointer; }
|
|
8793
|
+
p.small { font-size: 0.85rem; opacity: 0.75; }
|
|
8794
|
+
</style>
|
|
8795
|
+
</head>
|
|
8796
|
+
<body>
|
|
8797
|
+
<script>
|
|
8798
|
+
// The Console must remain this popup's opener until it finishes navigating
|
|
8799
|
+
// from about:blank. Safari rejects that cross-origin navigation otherwise.
|
|
8800
|
+
// Once this trusted local approval document has loaded, it needs no opener.
|
|
8801
|
+
window.opener = null;
|
|
8802
|
+
<\/script>
|
|
8803
|
+
<main>
|
|
8804
|
+
<h1>Allow this site to manage local MoltNet agents?</h1>
|
|
8805
|
+
<p><code>${escapeHtml(input.origin)}</code> asks to configure agents and start or stop local daemon runs on this machine.</p>
|
|
8806
|
+
<p class="small">Approve only if you opened that page yourself. This grant lasts until the local supervisor stops.</p>
|
|
8807
|
+
<form method="post" action="/pairings/${escapeHtml(input.pairingId)}/confirm">
|
|
8808
|
+
<input type="hidden" name="confirmToken" value="${escapeHtml(input.confirmToken)}" />
|
|
8809
|
+
<button type="submit">Approve</button>
|
|
8810
|
+
</form>
|
|
8811
|
+
</main>
|
|
8812
|
+
</body>
|
|
8813
|
+
</html>
|
|
8814
|
+
`;
|
|
8815
|
+
}
|
|
8816
|
+
function renderPairingResultPage(input) {
|
|
8817
|
+
return `<!doctype html>
|
|
8818
|
+
<html lang="en">
|
|
8819
|
+
<head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml(input.title)}</title>
|
|
8820
|
+
<style>:root{color-scheme:light dark}body{margin:0;font:16px/1.5 system-ui,sans-serif;display:grid;place-items:center;min-height:100vh;background:Canvas;color:CanvasText}main{max-width:26rem;padding:2rem}</style>
|
|
8821
|
+
</head>
|
|
8822
|
+
<body><main role="status"><h1>${escapeHtml(input.title)}</h1><p>${escapeHtml(input.message)}</p><p>You can close this tab.</p></main></body>
|
|
8823
|
+
</html>
|
|
8824
|
+
`;
|
|
8825
|
+
}
|
|
8826
|
+
//#endregion
|
|
8827
|
+
//#region src/lib/agent-server/provider-login.ts
|
|
8828
|
+
/**
|
|
8829
|
+
* Subscription-provider OAuth brokering for Agent Server (#2061 slice 4).
|
|
8830
|
+
*
|
|
8831
|
+
* The console clicks "Connect"; agent server runs the Pi OAuth flow host-side via
|
|
8832
|
+
* `ModelRuntime.login()` (which owns persistence into the shared
|
|
8833
|
+
* `pi/auth.json` and token rotation thereafter). The browser only ever sees
|
|
8834
|
+
* the provider's authorize URL or device code — never tokens.
|
|
8835
|
+
*
|
|
8836
|
+
* Provider ids come from Pi's model runtime, so Agent Server stays in lockstep
|
|
8837
|
+
* with supported subscription providers. GitHub Copilot is intentionally
|
|
8838
|
+
* excluded: MoltNet does not broker editor-seat subscription credentials.
|
|
8839
|
+
*/
|
|
8840
|
+
var LOGIN_TTL_MS = 600 * 1e3;
|
|
8841
|
+
/** How long `start()` waits for the flow to surface a URL / device code. */
|
|
8842
|
+
var START_INFO_TIMEOUT_MS = 5e3;
|
|
8843
|
+
var AgentServerSubscriptionError = class extends Error {
|
|
8844
|
+
name = "AgentServerSubscriptionError";
|
|
8845
|
+
constructor(code, message) {
|
|
8846
|
+
super(message);
|
|
8847
|
+
this.code = code;
|
|
8012
8848
|
}
|
|
8013
|
-
|
|
8014
|
-
|
|
8849
|
+
};
|
|
8850
|
+
var silentLogger = {
|
|
8851
|
+
info: () => void 0,
|
|
8852
|
+
warn: () => void 0,
|
|
8853
|
+
error: () => void 0
|
|
8854
|
+
};
|
|
8855
|
+
var ProviderLoginService = class ProviderLoginService {
|
|
8856
|
+
logins = /* @__PURE__ */ new Map();
|
|
8857
|
+
logger;
|
|
8858
|
+
constructor(options) {
|
|
8859
|
+
this.options = options;
|
|
8860
|
+
this.logger = options.logger ?? silentLogger;
|
|
8015
8861
|
}
|
|
8016
|
-
|
|
8017
|
-
|
|
8018
|
-
|
|
8019
|
-
|
|
8862
|
+
static async create(options) {
|
|
8863
|
+
const oauthProviders = await OAuthProviderService.create({
|
|
8864
|
+
authPath: options.authPath,
|
|
8865
|
+
logger: options.logger
|
|
8866
|
+
});
|
|
8867
|
+
return new ProviderLoginService({
|
|
8868
|
+
...options,
|
|
8869
|
+
oauthProviders
|
|
8020
8870
|
});
|
|
8021
|
-
writeJsonAtomic(this.agentPath(alias), config);
|
|
8022
|
-
if (!this.readIdentitySelector()?.default_identity) this.writeIdentitySelector(alias);
|
|
8023
8871
|
}
|
|
8024
|
-
|
|
8025
|
-
|
|
8026
|
-
this.clearIdentitySelectorIfDefault(alias);
|
|
8872
|
+
now() {
|
|
8873
|
+
return this.options.now?.() ?? Date.now();
|
|
8027
8874
|
}
|
|
8028
|
-
|
|
8029
|
-
|
|
8030
|
-
|
|
8875
|
+
providers() {
|
|
8876
|
+
return (this.options.listProviders ? this.options.listProviders() : this.options.oauthProviders ? this.options.oauthProviders.list() : this.runtime().getProviders().filter(isOAuthProviderEligible).map((provider) => ({
|
|
8877
|
+
id: provider.id,
|
|
8878
|
+
name: provider.name
|
|
8879
|
+
}))).filter((provider) => isOAuthProviderIdEligible(provider.id));
|
|
8880
|
+
}
|
|
8881
|
+
connected(providerId) {
|
|
8882
|
+
if (this.options.isConnected) return this.options.isConnected(providerId);
|
|
8883
|
+
if (this.options.oauthProviders) return this.options.oauthProviders.list().find((provider) => provider.id === providerId)?.connected ?? false;
|
|
8031
8884
|
try {
|
|
8032
|
-
|
|
8033
|
-
} catch {
|
|
8034
|
-
|
|
8885
|
+
return readStoredCredential(providerId, this.options.authPath) !== void 0;
|
|
8886
|
+
} catch (error) {
|
|
8887
|
+
this.logger.warn({
|
|
8888
|
+
event: "agent-server.subscription_auth_read_failed",
|
|
8889
|
+
providerId,
|
|
8890
|
+
...safeLoginError(error)
|
|
8891
|
+
}, "Could not read subscription authentication state");
|
|
8892
|
+
return false;
|
|
8035
8893
|
}
|
|
8036
|
-
if (!selector || selector.default_identity !== alias) return;
|
|
8037
|
-
writeJsonAtomic(this.identitySelectorPath, { version: 1 });
|
|
8038
|
-
}
|
|
8039
|
-
readActivation(alias) {
|
|
8040
|
-
return this.readAgentServerState().activations[assertStoreName("agent name", alias)] ?? null;
|
|
8041
|
-
}
|
|
8042
|
-
hasPendingRegistration(alias) {
|
|
8043
|
-
return Boolean(this.readAgentServerState().pendingRegistrations[assertStoreName("agent name", alias)]);
|
|
8044
|
-
}
|
|
8045
|
-
reserveRegistration(alias, apiUrl) {
|
|
8046
|
-
const name = assertStoreName("agent name", alias);
|
|
8047
|
-
const state = this.readAgentServerState();
|
|
8048
|
-
if (state.activations[name] || state.pendingRegistrations[name]) throw new AgentServerStoreError("already_exists", `agent "${name}" already exists in the agent server store`);
|
|
8049
|
-
state.pendingRegistrations[name] = {
|
|
8050
|
-
apiUrl,
|
|
8051
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8052
|
-
};
|
|
8053
|
-
this.writeAgentServerState(state);
|
|
8054
|
-
}
|
|
8055
|
-
clearPendingRegistration(alias) {
|
|
8056
|
-
const name = assertStoreName("agent name", alias);
|
|
8057
|
-
const state = this.readAgentServerState();
|
|
8058
|
-
delete state.pendingRegistrations[name];
|
|
8059
|
-
this.writeAgentServerState(state);
|
|
8060
|
-
}
|
|
8061
|
-
writeActivation(activation) {
|
|
8062
|
-
const alias = assertStoreName("agent name", activation.alias);
|
|
8063
|
-
validateActivation(alias, activation);
|
|
8064
|
-
const state = this.readAgentServerState();
|
|
8065
|
-
state.activations[alias] = activation;
|
|
8066
|
-
if (activation.source === "managed") delete state.pendingRegistrations[alias];
|
|
8067
|
-
this.writeAgentServerState(state);
|
|
8068
8894
|
}
|
|
8069
|
-
|
|
8070
|
-
|
|
8071
|
-
|
|
8072
|
-
get providersPath() {
|
|
8073
|
-
return join(this.root, "providers.json");
|
|
8895
|
+
runtime() {
|
|
8896
|
+
if (!this.options.modelRuntime) throw new Error("ProviderLoginService requires a model runtime when production adapters are not overridden");
|
|
8897
|
+
return this.options.modelRuntime;
|
|
8074
8898
|
}
|
|
8075
|
-
|
|
8076
|
-
|
|
8077
|
-
this.
|
|
8078
|
-
return
|
|
8899
|
+
list() {
|
|
8900
|
+
this.sweep();
|
|
8901
|
+
if (this.options.oauthProviders && !this.options.listProviders && !this.options.isConnected) return this.options.oauthProviders.list();
|
|
8902
|
+
return this.providers().map((provider) => ({
|
|
8903
|
+
...provider,
|
|
8904
|
+
connected: this.connected(provider.id)
|
|
8905
|
+
}));
|
|
8079
8906
|
}
|
|
8080
|
-
|
|
8081
|
-
this.
|
|
8082
|
-
|
|
8907
|
+
sweep() {
|
|
8908
|
+
const now = this.now();
|
|
8909
|
+
for (const [id, login] of this.logins) if (login.startedAt + LOGIN_TTL_MS <= now) {
|
|
8910
|
+
if (login.status === "pending") this.invalidate(login, "expired");
|
|
8911
|
+
this.logins.delete(id);
|
|
8912
|
+
}
|
|
8083
8913
|
}
|
|
8084
|
-
|
|
8085
|
-
|
|
8086
|
-
|
|
8087
|
-
|
|
8914
|
+
restoreCredential(login) {
|
|
8915
|
+
try {
|
|
8916
|
+
restoreStoredCredential(this.options.authPath, login.providerId, login.previousCredential);
|
|
8917
|
+
return true;
|
|
8918
|
+
} catch (error) {
|
|
8919
|
+
this.logger.error({
|
|
8920
|
+
event: "agent-server.subscription_login_cleanup_failed",
|
|
8921
|
+
operationId: login.operationId,
|
|
8922
|
+
providerId: login.providerId,
|
|
8923
|
+
...safeLoginError(error)
|
|
8924
|
+
}, "Could not restore subscription credentials after an invalidated login");
|
|
8925
|
+
return false;
|
|
8088
8926
|
}
|
|
8089
8927
|
}
|
|
8090
|
-
|
|
8091
|
-
|
|
8928
|
+
invalidate(login, transition) {
|
|
8929
|
+
if (login.invalidated) return true;
|
|
8930
|
+
login.invalidated = true;
|
|
8931
|
+
login.abort.abort(/* @__PURE__ */ new Error(`subscription login ${transition}`));
|
|
8932
|
+
const restored = this.restoreCredential(login);
|
|
8933
|
+
login.cleanupSucceeded = restored;
|
|
8934
|
+
login.infoArrived();
|
|
8935
|
+
this.logger.info({
|
|
8936
|
+
event: "agent-server.subscription_login_transition",
|
|
8937
|
+
operationId: login.operationId,
|
|
8938
|
+
providerId: login.providerId,
|
|
8939
|
+
transition
|
|
8940
|
+
}, "Subscription login invalidated");
|
|
8941
|
+
return restored;
|
|
8942
|
+
}
|
|
8943
|
+
status(providerId) {
|
|
8944
|
+
this.sweep();
|
|
8945
|
+
const login = this.logins.get(providerId);
|
|
8946
|
+
if (!login) throw new AgentServerSubscriptionError("login_not_found", `no login in progress for "${providerId}"`);
|
|
8947
|
+
return snapshot(login);
|
|
8092
8948
|
}
|
|
8093
|
-
|
|
8094
|
-
|
|
8095
|
-
|
|
8096
|
-
|
|
8097
|
-
|
|
8098
|
-
|
|
8099
|
-
|
|
8100
|
-
|
|
8101
|
-
|
|
8102
|
-
if (
|
|
8103
|
-
|
|
8104
|
-
|
|
8105
|
-
|
|
8106
|
-
|
|
8107
|
-
|
|
8108
|
-
|
|
8109
|
-
|
|
8110
|
-
|
|
8949
|
+
/**
|
|
8950
|
+
* Start (or return the in-flight) login for a provider. Resolves once the
|
|
8951
|
+
* flow has surfaced an authorize URL / device code, completed, or the
|
|
8952
|
+
* start window elapsed — whichever comes first.
|
|
8953
|
+
*/
|
|
8954
|
+
async start(providerId) {
|
|
8955
|
+
this.sweep();
|
|
8956
|
+
if (!this.providers().some((provider) => provider.id === providerId)) throw new AgentServerSubscriptionError("provider_unknown", `"${providerId}" is not a known subscription provider`);
|
|
8957
|
+
const existing = this.logins.get(providerId);
|
|
8958
|
+
if (existing && existing.status === "pending") return snapshot(existing);
|
|
8959
|
+
let infoArrived = () => void 0;
|
|
8960
|
+
const infoPromise = new Promise((resolvePromise) => {
|
|
8961
|
+
infoArrived = () => resolvePromise();
|
|
8962
|
+
});
|
|
8963
|
+
const abort = new AbortController();
|
|
8964
|
+
const login = {
|
|
8965
|
+
providerId,
|
|
8966
|
+
status: "pending",
|
|
8967
|
+
operationId: randomUUID(),
|
|
8968
|
+
startedAt: this.now(),
|
|
8969
|
+
infoArrived,
|
|
8970
|
+
abort,
|
|
8971
|
+
invalidated: false,
|
|
8972
|
+
cleanupUnderProviderLock: !this.options.runLogin && Boolean(this.options.oauthProviders),
|
|
8973
|
+
cleanupSucceeded: true,
|
|
8974
|
+
previousCredential: readStoredCredential(providerId, this.options.authPath)
|
|
8975
|
+
};
|
|
8976
|
+
this.logins.set(providerId, login);
|
|
8977
|
+
this.logger.info({
|
|
8978
|
+
event: "agent-server.subscription_login_transition",
|
|
8979
|
+
operationId: login.operationId,
|
|
8980
|
+
providerId,
|
|
8981
|
+
transition: "started"
|
|
8982
|
+
}, "Subscription login started");
|
|
8983
|
+
const callbacks = createLoginCallbacks(login, this.logger);
|
|
8984
|
+
(this.options.runLogin ?? ((id, loginCallbacks) => {
|
|
8985
|
+
if (this.options.oauthProviders) return this.options.oauthProviders.login(id, toAuthInteraction(loginCallbacks), {
|
|
8986
|
+
signal: login.abort.signal,
|
|
8987
|
+
onSettledUnderLock: () => {
|
|
8988
|
+
if (login.invalidated) login.cleanupSucceeded = this.restoreCredential(login);
|
|
8989
|
+
}
|
|
8990
|
+
});
|
|
8991
|
+
return this.runtime().login(id, "oauth", toAuthInteraction(loginCallbacks)).then(() => void 0);
|
|
8992
|
+
}))(providerId, callbacks).then(() => {
|
|
8993
|
+
if (login.invalidated) {
|
|
8994
|
+
if (!login.cleanupUnderProviderLock) this.restoreCredential(login);
|
|
8995
|
+
return;
|
|
8996
|
+
}
|
|
8997
|
+
if (!this.options.runLogin && !this.connected(providerId)) {
|
|
8998
|
+
login.status = "failed";
|
|
8999
|
+
login.error = "Subscription sign-in completed, but credentials were not persisted. Start again to retry.";
|
|
9000
|
+
this.logger.error({
|
|
9001
|
+
event: "agent-server.subscription_login_transition",
|
|
9002
|
+
operationId: login.operationId,
|
|
9003
|
+
providerId,
|
|
9004
|
+
transition: "persistence_failed"
|
|
9005
|
+
}, "Subscription login credentials were not persisted");
|
|
9006
|
+
} else {
|
|
9007
|
+
login.status = "completed";
|
|
9008
|
+
this.logger.info({
|
|
9009
|
+
event: "agent-server.subscription_login_transition",
|
|
9010
|
+
operationId: login.operationId,
|
|
9011
|
+
providerId,
|
|
9012
|
+
transition: "completed"
|
|
9013
|
+
}, "Subscription login completed");
|
|
9014
|
+
}
|
|
9015
|
+
login.infoArrived();
|
|
9016
|
+
}, (error) => {
|
|
9017
|
+
if (login.invalidated) {
|
|
9018
|
+
if (!login.cleanupUnderProviderLock) this.restoreCredential(login);
|
|
9019
|
+
return;
|
|
9020
|
+
}
|
|
9021
|
+
login.status = "failed";
|
|
9022
|
+
login.error = publicLoginError(error);
|
|
9023
|
+
this.logger.warn({
|
|
9024
|
+
event: "agent-server.subscription_login_transition",
|
|
9025
|
+
operationId: login.operationId,
|
|
9026
|
+
providerId,
|
|
9027
|
+
transition: "failed",
|
|
9028
|
+
...safeLoginError(error)
|
|
9029
|
+
}, "Subscription login failed");
|
|
9030
|
+
login.infoArrived();
|
|
9031
|
+
});
|
|
9032
|
+
if (!await Promise.race([infoPromise, new Promise((resolvePromise) => {
|
|
9033
|
+
setTimeout(() => resolvePromise(), START_INFO_TIMEOUT_MS).unref?.();
|
|
9034
|
+
})]).then(() => login.authUrl !== void 0 || login.userCode !== void 0 || login.status !== "pending")) {
|
|
9035
|
+
login.waitingForAuthorization = true;
|
|
9036
|
+
this.logger.warn({
|
|
9037
|
+
event: "serve.subscription_login_start_info_timeout",
|
|
9038
|
+
operationId: login.operationId,
|
|
9039
|
+
providerId,
|
|
9040
|
+
timeoutMs: START_INFO_TIMEOUT_MS
|
|
9041
|
+
}, "Subscription login is still waiting for authorization information");
|
|
8111
9042
|
}
|
|
8112
|
-
return
|
|
9043
|
+
return snapshot(login);
|
|
8113
9044
|
}
|
|
8114
|
-
|
|
8115
|
-
|
|
8116
|
-
const
|
|
8117
|
-
|
|
8118
|
-
|
|
8119
|
-
|
|
8120
|
-
});
|
|
9045
|
+
/** Abort an in-flight login and forget it. */
|
|
9046
|
+
cancel(providerId) {
|
|
9047
|
+
const login = this.logins.get(providerId);
|
|
9048
|
+
if (!login) throw new AgentServerSubscriptionError("login_not_found", `no login in progress for "${providerId}"`);
|
|
9049
|
+
if (!this.invalidate(login, "cancelled")) throw new AgentServerSubscriptionError("login_cleanup_failed", `could not safely cancel login for "${providerId}"; credential cleanup requires intervention`);
|
|
9050
|
+
this.logins.delete(providerId);
|
|
8121
9051
|
return {
|
|
8122
|
-
|
|
8123
|
-
|
|
8124
|
-
logPath: join(dir, "daemon.log")
|
|
9052
|
+
providerId,
|
|
9053
|
+
status: "cancelled"
|
|
8125
9054
|
};
|
|
8126
9055
|
}
|
|
8127
|
-
|
|
8128
|
-
|
|
8129
|
-
|
|
8130
|
-
|
|
8131
|
-
writeJsonAtomic(join(this.runDir(record.id), "run.json"), record);
|
|
8132
|
-
}
|
|
8133
|
-
listRuns(limit = Number.POSITIVE_INFINITY) {
|
|
8134
|
-
let ids;
|
|
8135
|
-
try {
|
|
8136
|
-
ids = readdirSync(this.runsDir);
|
|
8137
|
-
} catch {
|
|
8138
|
-
return [];
|
|
8139
|
-
}
|
|
8140
|
-
const sortedIds = ids.filter((id) => NAME_RE.test(id)).sort().reverse();
|
|
8141
|
-
const selectedIds = Number.isFinite(limit) ? sortedIds.slice(0, Math.max(0, limit)) : sortedIds;
|
|
8142
|
-
const records = [];
|
|
8143
|
-
for (const id of selectedIds) {
|
|
8144
|
-
const record = this.readRun(id);
|
|
8145
|
-
if (record) records.push(record);
|
|
8146
|
-
}
|
|
8147
|
-
return records.sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
9056
|
+
/** Abort every pending flow during supervisor shutdown. */
|
|
9057
|
+
close() {
|
|
9058
|
+
for (const login of this.logins.values()) if (login.status === "pending") this.invalidate(login, "shutdown");
|
|
9059
|
+
this.logins.clear();
|
|
8148
9060
|
}
|
|
8149
|
-
|
|
8150
|
-
|
|
8151
|
-
|
|
8152
|
-
|
|
8153
|
-
|
|
8154
|
-
|
|
8155
|
-
|
|
8156
|
-
|
|
8157
|
-
|
|
8158
|
-
|
|
8159
|
-
|
|
8160
|
-
|
|
8161
|
-
|
|
8162
|
-
|
|
8163
|
-
|
|
8164
|
-
|
|
8165
|
-
|
|
8166
|
-
|
|
8167
|
-
|
|
8168
|
-
|
|
8169
|
-
|
|
9061
|
+
};
|
|
9062
|
+
function toAuthInteraction(callbacks) {
|
|
9063
|
+
return {
|
|
9064
|
+
...callbacks.signal ? { signal: callbacks.signal } : {},
|
|
9065
|
+
notify: (event) => {
|
|
9066
|
+
switch (event.type) {
|
|
9067
|
+
case "auth_url":
|
|
9068
|
+
callbacks.onAuth({
|
|
9069
|
+
url: event.url,
|
|
9070
|
+
...event.instructions ? { instructions: event.instructions } : {}
|
|
9071
|
+
});
|
|
9072
|
+
break;
|
|
9073
|
+
case "device_code":
|
|
9074
|
+
callbacks.onDeviceCode({
|
|
9075
|
+
userCode: event.userCode,
|
|
9076
|
+
verificationUri: event.verificationUri
|
|
9077
|
+
});
|
|
9078
|
+
break;
|
|
9079
|
+
case "info":
|
|
9080
|
+
case "progress":
|
|
9081
|
+
callbacks.onProgress?.(event.message);
|
|
9082
|
+
break;
|
|
8170
9083
|
}
|
|
8171
|
-
|
|
8172
|
-
|
|
9084
|
+
},
|
|
9085
|
+
prompt: async (prompt) => {
|
|
9086
|
+
if (prompt.type !== "select") return callbacks.onPrompt({ message: prompt.message });
|
|
9087
|
+
const selected = await callbacks.onSelect({
|
|
9088
|
+
message: prompt.message,
|
|
9089
|
+
options: prompt.options.map(({ id, label }) => ({
|
|
9090
|
+
id,
|
|
9091
|
+
label
|
|
9092
|
+
}))
|
|
9093
|
+
});
|
|
9094
|
+
if (!selected) throw new AgentServerSubscriptionError("login_unsupported_prompt", "This provider flow did not offer a supported sign-in method");
|
|
9095
|
+
return selected;
|
|
8173
9096
|
}
|
|
8174
|
-
|
|
9097
|
+
};
|
|
9098
|
+
}
|
|
9099
|
+
function restoreStoredCredential(authPath, providerId, credential) {
|
|
9100
|
+
mkdirSync(dirname(authPath), {
|
|
9101
|
+
recursive: true,
|
|
9102
|
+
mode: 448
|
|
9103
|
+
});
|
|
9104
|
+
try {
|
|
9105
|
+
writeFileSync(authPath, "{}\n", {
|
|
9106
|
+
flag: "wx",
|
|
9107
|
+
mode: 384
|
|
9108
|
+
});
|
|
9109
|
+
} catch (error) {
|
|
9110
|
+
if (error.code !== "EEXIST") throw error;
|
|
8175
9111
|
}
|
|
8176
|
-
};
|
|
8177
|
-
|
|
8178
|
-
let info;
|
|
9112
|
+
const release = lockSync(authPath, { realpath: false });
|
|
9113
|
+
const temp = `${authPath}.${randomBytes(6).toString("hex")}.tmp`;
|
|
8179
9114
|
try {
|
|
8180
|
-
|
|
8181
|
-
|
|
8182
|
-
|
|
9115
|
+
const parsed = JSON.parse(readFileSync(authPath, "utf8"));
|
|
9116
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`Pi credential store is not an object: ${authPath}`);
|
|
9117
|
+
const credentials = parsed;
|
|
9118
|
+
if (credential) credentials[providerId] = credential;
|
|
9119
|
+
else delete credentials[providerId];
|
|
9120
|
+
writeFileSync(temp, `${JSON.stringify(credentials, null, 2)}\n`, { mode: 384 });
|
|
9121
|
+
renameSync(temp, authPath);
|
|
9122
|
+
} finally {
|
|
9123
|
+
rmSync(temp, { force: true });
|
|
9124
|
+
release();
|
|
8183
9125
|
}
|
|
8184
|
-
if (info.isSymbolicLink()) return 0;
|
|
8185
|
-
if (!info.isDirectory()) return info.size;
|
|
8186
|
-
let total = 0;
|
|
8187
|
-
for (const entry of readdirSync(path, { withFileTypes: true })) total += directoryBytes(join(path, entry.name));
|
|
8188
|
-
return total;
|
|
8189
9126
|
}
|
|
8190
|
-
function
|
|
8191
|
-
return
|
|
9127
|
+
function createLoginCallbacks(login, logger) {
|
|
9128
|
+
return {
|
|
9129
|
+
onAuth: (info) => {
|
|
9130
|
+
login.authUrl = info.url;
|
|
9131
|
+
if (info.instructions) login.instructions = info.instructions;
|
|
9132
|
+
logger.info({
|
|
9133
|
+
event: "agent-server.subscription_login_transition",
|
|
9134
|
+
operationId: login.operationId,
|
|
9135
|
+
providerId: login.providerId,
|
|
9136
|
+
transition: "authorization_ready"
|
|
9137
|
+
}, "Subscription authorization URL ready");
|
|
9138
|
+
login.infoArrived();
|
|
9139
|
+
},
|
|
9140
|
+
onDeviceCode: (info) => {
|
|
9141
|
+
login.userCode = info.userCode;
|
|
9142
|
+
login.verificationUri = info.verificationUri;
|
|
9143
|
+
logger.info({
|
|
9144
|
+
event: "agent-server.subscription_login_transition",
|
|
9145
|
+
operationId: login.operationId,
|
|
9146
|
+
providerId: login.providerId,
|
|
9147
|
+
transition: "device_code_ready"
|
|
9148
|
+
}, "Subscription device code ready");
|
|
9149
|
+
login.infoArrived();
|
|
9150
|
+
},
|
|
9151
|
+
onPrompt: () => Promise.reject(new AgentServerSubscriptionError("login_unsupported_prompt", "This provider flow needs an interactive prompt; run `pi /login` in a terminal instead")),
|
|
9152
|
+
onSelect: (prompt) => Promise.resolve((prompt.options.find((option) => /device/i.test(option.id)) ?? prompt.options[0])?.id),
|
|
9153
|
+
signal: login.abort.signal
|
|
9154
|
+
};
|
|
8192
9155
|
}
|
|
8193
|
-
function
|
|
8194
|
-
|
|
8195
|
-
|
|
8196
|
-
const candidate = resolve(normalizedRoot, `${name}${suffix}`);
|
|
8197
|
-
if (!isStrictDescendant(normalizedRoot, candidate)) throw new AgentServerStoreError("invalid_name", `${kind} escapes its store`);
|
|
8198
|
-
return candidate;
|
|
9156
|
+
function publicLoginError(error) {
|
|
9157
|
+
if (error instanceof AgentServerSubscriptionError) return error.message;
|
|
9158
|
+
return "Subscription sign-in failed. Start again to retry.";
|
|
8199
9159
|
}
|
|
8200
|
-
function
|
|
8201
|
-
const
|
|
8202
|
-
|
|
9160
|
+
function safeLoginError(error) {
|
|
9161
|
+
const result = { errorType: error instanceof Error ? error.name : typeof error };
|
|
9162
|
+
const code = error?.code;
|
|
9163
|
+
if (typeof code === "string" && /^[a-z0-9_:-]{1,64}$/iu.test(code)) result["applicationCode"] = code;
|
|
9164
|
+
return result;
|
|
8203
9165
|
}
|
|
8204
|
-
function
|
|
8205
|
-
const
|
|
8206
|
-
|
|
9166
|
+
function snapshot(login) {
|
|
9167
|
+
const { providerId, status, authUrl, instructions, userCode, verificationUri, error, waitingForAuthorization } = login;
|
|
9168
|
+
return {
|
|
9169
|
+
providerId,
|
|
9170
|
+
status,
|
|
9171
|
+
...authUrl ? { authUrl } : {},
|
|
9172
|
+
...instructions ? { instructions } : {},
|
|
9173
|
+
...userCode ? { userCode } : {},
|
|
9174
|
+
...verificationUri ? { verificationUri } : {},
|
|
9175
|
+
...error ? { error } : {},
|
|
9176
|
+
...waitingForAuthorization ? { waitingForAuthorization } : {}
|
|
8207
9177
|
};
|
|
8208
|
-
if (!isRecord$1(value)) invalid();
|
|
8209
|
-
const activation = value;
|
|
8210
|
-
if (activation.alias !== alias) invalid();
|
|
8211
|
-
if (![
|
|
8212
|
-
"subjectId",
|
|
8213
|
-
"publicKey",
|
|
8214
|
-
"fingerprint",
|
|
8215
|
-
"createdAt"
|
|
8216
|
-
].every((field) => typeof activation[field] === "string" && activation[field].length > 0)) invalid();
|
|
8217
|
-
if (activation.source === "managed") {
|
|
8218
|
-
if (typeof activation.apiUrl !== "string" || activation.apiUrl.length === 0 || activation.configPath !== void 0 || activation.configApiUrl !== void 0) invalid();
|
|
8219
|
-
return;
|
|
8220
|
-
}
|
|
8221
|
-
if (activation.source !== "external" || typeof activation.configPath !== "string" || activation.configPath.length === 0 || typeof activation.configApiUrl !== "string" || activation.configApiUrl.length === 0 || activation.apiUrl !== void 0 && typeof activation.apiUrl !== "string") invalid();
|
|
8222
9178
|
}
|
|
8223
9179
|
//#endregion
|
|
8224
9180
|
//#region src/lib/agent-server/identity.ts
|
|
@@ -8240,13 +9196,13 @@ var AgentServerIdentityError = class extends Error {
|
|
|
8240
9196
|
this.code = code;
|
|
8241
9197
|
}
|
|
8242
9198
|
};
|
|
8243
|
-
function reserveAlias(store, alias) {
|
|
9199
|
+
function reserveAlias(store, alias, allowExistingConfig = false) {
|
|
8244
9200
|
let pending = pendingAliases.get(store);
|
|
8245
9201
|
if (!pending) {
|
|
8246
9202
|
pending = /* @__PURE__ */ new Set();
|
|
8247
9203
|
pendingAliases.set(store, pending);
|
|
8248
9204
|
}
|
|
8249
|
-
if (pending.has(alias) || store.hasPendingRegistration(alias) || store.readActivation(alias) || store.readAgentConfig(alias)) throw new AgentServerIdentityError("agent_exists", `agent "${alias}" already exists in the agent server store`);
|
|
9205
|
+
if (pending.has(alias) || store.hasPendingRegistration(alias) || store.readActivation(alias) || !allowExistingConfig && store.readAgentConfig(alias)) throw new AgentServerIdentityError("agent_exists", `agent "${alias}" already exists in the agent server store`);
|
|
8250
9206
|
pending.add(alias);
|
|
8251
9207
|
return () => pending.delete(alias);
|
|
8252
9208
|
}
|
|
@@ -8388,15 +9344,16 @@ async function reconcileManagedRegistration(store, secrets, aliasInput, action,
|
|
|
8388
9344
|
}
|
|
8389
9345
|
async function attachExternalAgent(store, secretProviders, input, connectAgent = connect) {
|
|
8390
9346
|
const alias = assertStoreName("agent name", input.name);
|
|
8391
|
-
|
|
9347
|
+
if (!isAbsolute(input.configDir)) throw new AgentServerIdentityError("config_not_found", "external configDir must be an absolute path");
|
|
9348
|
+
const configPath = join(input.configDir, "moltnet.json");
|
|
9349
|
+
const central = configPath === store.agentPath(alias);
|
|
9350
|
+
const releaseAlias = reserveAlias(store, alias, central);
|
|
8392
9351
|
try {
|
|
8393
|
-
if (!
|
|
8394
|
-
const configPath = join(input.configDir, "moltnet.json");
|
|
8395
|
-
externalAgentLocation(configPath);
|
|
9352
|
+
if (!central) externalAgentLocation(configPath);
|
|
8396
9353
|
const config = await readCurrentConfig(configPath);
|
|
9354
|
+
if (central && !config.agent_key_ref) throw new AgentServerIdentityError("unsupported_credential", `central identity "${alias}" needs a stored agent key before the Agent Server can run it`);
|
|
8397
9355
|
const configApiUrl = requireTrustedConfigApiUrl(config, configPath);
|
|
8398
|
-
const
|
|
8399
|
-
const whoami = await authenticateConfig(input.configDir, effectiveApiUrl, secretProviders, connectAgent, input.signal);
|
|
9356
|
+
const whoami = await authenticateConfig(config, configPath, requireTrustedApiOverride(input.apiUrl, configApiUrl, configPath), secretProviders, connectAgent, input.signal);
|
|
8400
9357
|
const identity = identityFromConfig(config);
|
|
8401
9358
|
assertIdentityMatches(identity, whoami, `external config ${configPath}`, "authenticated whoami");
|
|
8402
9359
|
assertSubjectMatches(whoami, config, "authenticated whoami", `external config ${configPath}`);
|
|
@@ -8425,7 +9382,7 @@ async function attachExternalAgent(store, secretProviders, input, connectAgent =
|
|
|
8425
9382
|
/** Load and authenticate the current config, then refresh its derived pin. */
|
|
8426
9383
|
async function verifyAgentActivation(store, alias, managedSecretProviders, externalSecretProviders, connectAgent = connect, signal) {
|
|
8427
9384
|
const activation = requireActivation(store, alias);
|
|
8428
|
-
const verified = activation.source === "managed" ? await verifyManagedActivation(store, activation, managedSecretProviders, connectAgent, signal) : await verifyExternalActivation(activation, externalSecretProviders, connectAgent, signal);
|
|
9385
|
+
const verified = activation.source === "managed" ? await verifyManagedActivation(store, activation, managedSecretProviders, connectAgent, signal) : await verifyExternalActivation(store, activation, externalSecretProviders, connectAgent, signal);
|
|
8429
9386
|
assertSubjectMatches(verified.whoami, verified.config, "authenticated whoami", `agent "${activation.alias}" config`);
|
|
8430
9387
|
if (verified.whoami.subjectId !== activation.subjectId) throw new AgentServerIdentityError("verification_failed", `authenticated whoami subject does not match agent "${activation.alias}" pinned activation`);
|
|
8431
9388
|
const identity = identityFromConfig(verified.config);
|
|
@@ -8465,15 +9422,17 @@ async function verifyManagedActivation(store, activation, secretProviders, conne
|
|
|
8465
9422
|
}, configPath, signal)
|
|
8466
9423
|
};
|
|
8467
9424
|
}
|
|
8468
|
-
async function verifyExternalActivation(activation, secretProviders, connectAgent, signal) {
|
|
8469
|
-
|
|
9425
|
+
async function verifyExternalActivation(store, activation, secretProviders, connectAgent, signal) {
|
|
9426
|
+
const central = activation.configPath === store.agentPath(activation.alias);
|
|
9427
|
+
if (!central) externalAgentLocation(activation.configPath);
|
|
8470
9428
|
assertTrustedConfigApiUrl(activation.configApiUrl);
|
|
8471
9429
|
const config = await readCurrentConfig(activation.configPath);
|
|
9430
|
+
if (central && !config.agent_key_ref) throw new AgentServerIdentityError("unsupported_credential", `central identity "${activation.alias}" needs a stored agent key before the Agent Server can run it`);
|
|
8472
9431
|
assertActivatedConfig(config, activation, activation.configPath, requireTrustedConfigApiUrl(config, activation.configPath), activation.configApiUrl);
|
|
8473
9432
|
const effectiveApiUrl = requireTrustedApiOverride(activation.apiUrl, activation.configApiUrl, activation.configPath);
|
|
8474
9433
|
return {
|
|
8475
9434
|
config,
|
|
8476
|
-
whoami: await authenticateConfig(
|
|
9435
|
+
whoami: await authenticateConfig(config, activation.configPath, effectiveApiUrl, secretProviders, connectAgent, signal)
|
|
8477
9436
|
};
|
|
8478
9437
|
}
|
|
8479
9438
|
/** Never let request or activation metadata redirect persisted credentials. */
|
|
@@ -8533,12 +9492,18 @@ function externalAgentLocation(configPath) {
|
|
|
8533
9492
|
agentRoot: dirname(moltnetDir)
|
|
8534
9493
|
};
|
|
8535
9494
|
}
|
|
8536
|
-
async function authenticateConfig(
|
|
9495
|
+
async function authenticateConfig(config, configPath, apiUrl, secretProviders, connectAgent, signal) {
|
|
9496
|
+
let agentKey;
|
|
9497
|
+
try {
|
|
9498
|
+
agentKey = await resolveAgentKey(config, secretProviders);
|
|
9499
|
+
} catch (cause) {
|
|
9500
|
+
throw verificationError(`could not resolve the daemon agent key from ${configPath}`, cause);
|
|
9501
|
+
}
|
|
9502
|
+
if (!agentKey) throw new AgentServerIdentityError("unsupported_credential", `external daemon config at ${configPath} must contain an agent_key_ref`);
|
|
8537
9503
|
return callWhoami(connectAgent, {
|
|
8538
|
-
|
|
8539
|
-
...apiUrl ? { apiUrl } : {}
|
|
8540
|
-
|
|
8541
|
-
}, configDir, signal);
|
|
9504
|
+
agentKey,
|
|
9505
|
+
...apiUrl ? { apiUrl } : {}
|
|
9506
|
+
}, configPath, signal);
|
|
8542
9507
|
}
|
|
8543
9508
|
async function callWhoami(connectAgent, options, source, signal) {
|
|
8544
9509
|
try {
|
|
@@ -8663,6 +9628,8 @@ function validateRunSpec(spec) {
|
|
|
8663
9628
|
if (!spec.teamId) throw new AgentServerRunError("invalid_spec", "teamId is required");
|
|
8664
9629
|
if (!Array.isArray(spec.profiles) || spec.profiles.length === 0) throw new AgentServerRunError("invalid_spec", "at least one profile is required");
|
|
8665
9630
|
if (!Array.isArray(spec.taskTypes) || spec.taskTypes.length === 0) throw new AgentServerRunError("invalid_spec", "at least one task type is required");
|
|
9631
|
+
const unknownTaskType = spec.taskTypes.find((taskType) => !Object.prototype.hasOwnProperty.call(BUILT_IN_TASK_TYPES, taskType));
|
|
9632
|
+
if (unknownTaskType) throw new AgentServerRunError("invalid_spec", `unknown task type "${unknownTaskType}"`);
|
|
8666
9633
|
}
|
|
8667
9634
|
var RunManager = class {
|
|
8668
9635
|
active = /* @__PURE__ */ new Map();
|
|
@@ -8700,6 +9667,10 @@ var RunManager = class {
|
|
|
8700
9667
|
agentName: activation.alias,
|
|
8701
9668
|
cwd: dirname(piDir),
|
|
8702
9669
|
extraArgs: []
|
|
9670
|
+
} : activation.configPath === this.store.agentPath(activation.alias) ? {
|
|
9671
|
+
agentName: activation.alias,
|
|
9672
|
+
cwd: dirname(piDir),
|
|
9673
|
+
extraArgs: []
|
|
8703
9674
|
} : (() => {
|
|
8704
9675
|
const { agentName, agentRoot } = externalAgentLocation(activation.configPath);
|
|
8705
9676
|
return {
|
|
@@ -8730,14 +9701,9 @@ var RunManager = class {
|
|
|
8730
9701
|
env["MOLTNET_API_URL"] = activation.apiUrl ?? activation.configApiUrl;
|
|
8731
9702
|
try {
|
|
8732
9703
|
const agentKey = await resolveAgentKey(config, this.options.externalSecretProviders);
|
|
8733
|
-
if (agentKey)
|
|
8734
|
-
|
|
8735
|
-
|
|
8736
|
-
} else if (config.oauth2?.client_secret_ref) {
|
|
8737
|
-
env["MOLTNET_CLIENT_ID"] = config.oauth2.client_id;
|
|
8738
|
-
env["MOLTNET_CLIENT_SECRET"] = await resolveOAuth2ClientSecret(config, this.options.externalSecretProviders);
|
|
8739
|
-
}
|
|
8740
|
-
if (!agentKey && config.keys.private_key_ref) env["MOLTNET_PRIVATE_KEY"] = await resolveIdentitySeed(config, this.options.externalSecretProviders);
|
|
9704
|
+
if (!agentKey) throw new Error("external daemon config has no agent key");
|
|
9705
|
+
env["MOLTNET_AGENT_KEY"] = agentKey;
|
|
9706
|
+
env["MOLTNET_PRIVATE_KEY"] = await resolveIdentitySeed(config, this.options.externalSecretProviders);
|
|
8741
9707
|
} catch {
|
|
8742
9708
|
throw new AgentServerRunError("invalid_spec", `external credentials for "${activation.alias}" could not be projected`);
|
|
8743
9709
|
}
|
|
@@ -8947,10 +9913,11 @@ var RunManager = class {
|
|
|
8947
9913
|
apiUrl: activation.apiUrl
|
|
8948
9914
|
});
|
|
8949
9915
|
}
|
|
9916
|
+
const agentKey = await resolveAgentKey(config, this.options.externalSecretProviders);
|
|
9917
|
+
if (!agentKey) throw new AgentServerRunError("invalid_spec", `external agent "${activation.alias}" has no agent key`);
|
|
8950
9918
|
return connect({
|
|
8951
|
-
|
|
8952
|
-
apiUrl: activation.apiUrl ?? activation.configApiUrl
|
|
8953
|
-
secretProviders: this.options.externalSecretProviders
|
|
9919
|
+
agentKey,
|
|
9920
|
+
apiUrl: activation.apiUrl ?? activation.configApiUrl
|
|
8954
9921
|
});
|
|
8955
9922
|
}
|
|
8956
9923
|
stop(id) {
|
|
@@ -9320,69 +10287,6 @@ function writeRegistry(path, entries) {
|
|
|
9320
10287
|
writeFileSync(temp, `${JSON.stringify(entries, null, 2)}\n`, { mode: 384 });
|
|
9321
10288
|
renameSync(temp, path);
|
|
9322
10289
|
}
|
|
9323
|
-
var AgentServerModelDiscoveryError = class extends Error {
|
|
9324
|
-
name = "AgentServerModelDiscoveryError";
|
|
9325
|
-
constructor(code, message, statusCode, options) {
|
|
9326
|
-
super(message, options);
|
|
9327
|
-
this.code = code;
|
|
9328
|
-
this.statusCode = statusCode;
|
|
9329
|
-
}
|
|
9330
|
-
};
|
|
9331
|
-
var ModelDiscoveryCollector = class {
|
|
9332
|
-
models = /* @__PURE__ */ new Set();
|
|
9333
|
-
addOpenAiResponse(value) {
|
|
9334
|
-
if (!isRecord(value) || !Array.isArray(value["data"])) return;
|
|
9335
|
-
for (const candidate of value["data"]) {
|
|
9336
|
-
if (!isRecord(candidate)) continue;
|
|
9337
|
-
const id = candidate["id"];
|
|
9338
|
-
if (typeof id === "string" && id.length > 0) this.models.add(id);
|
|
9339
|
-
}
|
|
9340
|
-
}
|
|
9341
|
-
addOllamaResponse(value) {
|
|
9342
|
-
if (!isRecord(value) || !Array.isArray(value["models"])) return;
|
|
9343
|
-
for (const candidate of value["models"]) {
|
|
9344
|
-
if (!isRecord(candidate)) continue;
|
|
9345
|
-
const name = candidate["name"];
|
|
9346
|
-
if (typeof name === "string" && name.length > 0) this.models.add(name);
|
|
9347
|
-
}
|
|
9348
|
-
}
|
|
9349
|
-
get size() {
|
|
9350
|
-
return this.models.size;
|
|
9351
|
-
}
|
|
9352
|
-
result(providerId, failures) {
|
|
9353
|
-
if (this.models.size === 0) throw discoveryFailure(providerId, failures);
|
|
9354
|
-
return {
|
|
9355
|
-
models: [...this.models].sort().slice(0, 500),
|
|
9356
|
-
discoveredCount: this.models.size
|
|
9357
|
-
};
|
|
9358
|
-
}
|
|
9359
|
-
};
|
|
9360
|
-
function parseProviderBaseUrl(value, providerId) {
|
|
9361
|
-
let parsed;
|
|
9362
|
-
try {
|
|
9363
|
-
parsed = new URL(value);
|
|
9364
|
-
} catch (cause) {
|
|
9365
|
-
throw new AgentServerModelDiscoveryError("invalid_provider", `provider "${providerId}" has an invalid base URL`, 400, { cause });
|
|
9366
|
-
}
|
|
9367
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:" || parsed.username || parsed.password || parsed.search || parsed.hash) throw new AgentServerModelDiscoveryError("invalid_provider", `provider "${providerId}" base URL must be HTTP(S) without credentials, query, or fragment`, 400);
|
|
9368
|
-
if (isNonLoopbackPrivateAddress(parsed.hostname)) throw new AgentServerModelDiscoveryError("invalid_provider", `provider "${providerId}" base URL must not target a private network address`, 400);
|
|
9369
|
-
return parsed;
|
|
9370
|
-
}
|
|
9371
|
-
function isNonLoopbackPrivateAddress(hostname) {
|
|
9372
|
-
if (isIP(hostname) !== 4) return false;
|
|
9373
|
-
const [first, second] = hostname.split(".").map(Number);
|
|
9374
|
-
if (first === 127) return false;
|
|
9375
|
-
return first === 10 || first === 172 && second >= 16 && second <= 31 || first === 192 && second === 168 || first === 169 && second === 254;
|
|
9376
|
-
}
|
|
9377
|
-
function discoveryFailure(providerId, failures) {
|
|
9378
|
-
if (failures.some((failure) => failure.kind === "http" && (failure.status === 401 || failure.status === 403))) return new AgentServerModelDiscoveryError("discovery_unauthorized", `provider "${providerId}" rejected model discovery; check its API key`, 502);
|
|
9379
|
-
if (failures.some((failure) => failure.kind === "network")) return new AgentServerModelDiscoveryError("discovery_unavailable", `provider "${providerId}" could not be reached for model discovery`, 502);
|
|
9380
|
-
if (failures.some((failure) => failure.kind === "invalid_response")) return new AgentServerModelDiscoveryError("discovery_invalid_response", `provider "${providerId}" returned an invalid model response`, 502);
|
|
9381
|
-
return new AgentServerModelDiscoveryError("discovery_failed", `no models discovered for provider "${providerId}"`, 502);
|
|
9382
|
-
}
|
|
9383
|
-
function isRecord(value) {
|
|
9384
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9385
|
-
}
|
|
9386
10290
|
//#endregion
|
|
9387
10291
|
//#region src/lib/agent-server/protocol.ts
|
|
9388
10292
|
var DateTime = Type.String({ format: "date-time" });
|
|
@@ -9409,6 +10313,12 @@ var AgentServerAgentSchema = Type.Object({
|
|
|
9409
10313
|
hasAgentKey: Type.Optional(Type.Boolean()),
|
|
9410
10314
|
hasPrivateKey: Type.Optional(Type.Boolean())
|
|
9411
10315
|
}, { $id: "AgentServerAgent" });
|
|
10316
|
+
var AgentServerIdentitySchema = Type.Object({
|
|
10317
|
+
alias: Type.String(),
|
|
10318
|
+
activated: Type.Boolean(),
|
|
10319
|
+
hasAgentKey: Type.Boolean()
|
|
10320
|
+
}, { $id: "AgentServerIdentity" });
|
|
10321
|
+
var AgentServerTaskTypeSchema = Type.Union(REGISTERED_TASK_TYPES.map((taskType) => Type.Literal(taskType)), { $id: "AgentServerTaskType" });
|
|
9412
10322
|
var AgentServerProviderSchema = Type.Object({
|
|
9413
10323
|
api: Type.String(),
|
|
9414
10324
|
baseUrl: Type.String({ format: "uri" }),
|
|
@@ -9458,6 +10368,8 @@ var AgentServerStatusSchema = Type.Object({
|
|
|
9458
10368
|
platform: Type.String(),
|
|
9459
10369
|
subscriptions: Type.Array(schemaRef(AgentServerSubscriptionSchema)),
|
|
9460
10370
|
agents: Type.Array(schemaRef(AgentServerAgentSchema)),
|
|
10371
|
+
identities: Type.Array(schemaRef(AgentServerIdentitySchema)),
|
|
10372
|
+
selectedIdentity: Type.Optional(Type.String()),
|
|
9461
10373
|
providers: Type.Record(Type.String(), schemaRef(AgentServerProviderSchema)),
|
|
9462
10374
|
runs: Type.Array(schemaRef(AgentServerRunSchema))
|
|
9463
10375
|
}, { $id: "AgentServerStatus" });
|
|
@@ -9470,17 +10382,14 @@ var PairingParamsSchema = Type.Object({ pairingId: Type.String() });
|
|
|
9470
10382
|
var ProviderParamsSchema = Type.Object({ providerId: Type.String() });
|
|
9471
10383
|
var AgentParamsSchema = Type.Object({ agentName: Type.String() });
|
|
9472
10384
|
var RunParamsSchema = Type.Object({ runId: Type.String() });
|
|
9473
|
-
var CreateAgentSchema = Type.Object({
|
|
9474
|
-
kind: Type.Union([Type.Literal("managed"), Type.Literal("external")]),
|
|
9475
|
-
name: Type.String()
|
|
9476
|
-
}, { anyOf: [Type.Object({
|
|
10385
|
+
var CreateAgentSchema = Type.Union([Type.Object({
|
|
9477
10386
|
kind: Type.Literal("managed"),
|
|
10387
|
+
name: Type.String(),
|
|
9478
10388
|
enrollmentToken: Type.String()
|
|
9479
10389
|
}), Type.Object({
|
|
9480
10390
|
kind: Type.Literal("external"),
|
|
9481
|
-
|
|
9482
|
-
|
|
9483
|
-
})] });
|
|
10391
|
+
identityAlias: Type.String()
|
|
10392
|
+
})]);
|
|
9484
10393
|
var ReconcileAgentSchema = Type.Object({ action: Type.Union([Type.Literal("resume"), Type.Literal("abandon")]) });
|
|
9485
10394
|
var ReconcileAgentResultSchema = Type.Union([schemaRef(AgentServerAgentSchema), Type.Object({ abandoned: Type.Literal(true) })], { $id: "ReconcileAgentResult" });
|
|
9486
10395
|
var PutProviderSchema = Type.Object({
|
|
@@ -9495,7 +10404,7 @@ var StartRunSchema = Type.Object({
|
|
|
9495
10404
|
agent: Type.String(),
|
|
9496
10405
|
teamId: Type.String(),
|
|
9497
10406
|
profiles: StringList,
|
|
9498
|
-
taskTypes:
|
|
10407
|
+
taskTypes: Type.Array(AgentServerTaskTypeSchema),
|
|
9499
10408
|
mode: Type.Union([Type.Literal("poll"), Type.Literal("drain")])
|
|
9500
10409
|
});
|
|
9501
10410
|
var CancelledSubscriptionSchema = Type.Object({
|
|
@@ -9510,6 +10419,8 @@ var AGENT_SERVER_SCHEMAS = [
|
|
|
9510
10419
|
AgentServerHealthSchema,
|
|
9511
10420
|
AgentServerProblemSchema,
|
|
9512
10421
|
AgentServerAgentSchema,
|
|
10422
|
+
AgentServerIdentitySchema,
|
|
10423
|
+
AgentServerTaskTypeSchema,
|
|
9513
10424
|
AgentServerProviderSchema,
|
|
9514
10425
|
AgentServerRunRecordSchema,
|
|
9515
10426
|
AgentServerRunSchema,
|
|
@@ -9805,8 +10716,8 @@ function stringArray(body, field, options = {}) {
|
|
|
9805
10716
|
}
|
|
9806
10717
|
function requestOperationSignal(request, shutdownSignal) {
|
|
9807
10718
|
const disconnected = new AbortController();
|
|
9808
|
-
if (request.raw.aborted) disconnected.abort();
|
|
9809
|
-
else request.raw.once("aborted", () => disconnected.abort());
|
|
10719
|
+
if (request.raw.aborted) disconnected.abort({ source: "request" });
|
|
10720
|
+
else request.raw.once("aborted", () => disconnected.abort({ source: "request" }));
|
|
9810
10721
|
return shutdownSignal ? AbortSignal.any([disconnected.signal, shutdownSignal]) : disconnected.signal;
|
|
9811
10722
|
}
|
|
9812
10723
|
function buildAgentServer(options) {
|
|
@@ -9925,12 +10836,15 @@ function registerStatusRoute(app, options, requirePairedOrigin) {
|
|
|
9925
10836
|
const { store, runs } = options;
|
|
9926
10837
|
app.get("/v1/status", { schema: AgentServerRouteSchemas.status }, async (request) => {
|
|
9927
10838
|
requirePairedOrigin(request);
|
|
10839
|
+
const selected = selectedIdentity(store, options.activeIdentity);
|
|
9928
10840
|
return {
|
|
9929
10841
|
version: options.version,
|
|
9930
10842
|
platform: process.platform,
|
|
9931
10843
|
subscriptions: options.subscriptions.list(),
|
|
9932
10844
|
agents: store.listActivations().map((activation) => publicAgentView(store, activation)),
|
|
9933
|
-
|
|
10845
|
+
identities: identityViews(store),
|
|
10846
|
+
...selected ? { selectedIdentity: selected } : {},
|
|
10847
|
+
providers: options.providers.list(),
|
|
9934
10848
|
runs: runViews(runs)
|
|
9935
10849
|
};
|
|
9936
10850
|
});
|
|
@@ -9960,11 +10874,10 @@ function registerAgentRoutes(app, options, requirePairedOrigin) {
|
|
|
9960
10874
|
return reply.code(201).send(publicAgentView(store, entry.activation));
|
|
9961
10875
|
}
|
|
9962
10876
|
if (kind === "external") {
|
|
9963
|
-
const
|
|
10877
|
+
const identityAlias = requireString(body, "identityAlias");
|
|
9964
10878
|
const entry = await attachExternalAgent(store, options.externalSecretProviders, {
|
|
9965
|
-
name:
|
|
9966
|
-
configDir:
|
|
9967
|
-
...apiUrl ? { apiUrl } : {},
|
|
10879
|
+
name: identityAlias,
|
|
10880
|
+
configDir: store.identityDir(identityAlias),
|
|
9968
10881
|
signal
|
|
9969
10882
|
});
|
|
9970
10883
|
return reply.code(201).send(publicAgentView(store, entry.activation));
|
|
@@ -9983,153 +10896,60 @@ function registerAgentRoutes(app, options, requirePairedOrigin) {
|
|
|
9983
10896
|
return reconciled ? publicAgentView(store, reconciled.activation) : { abandoned: true };
|
|
9984
10897
|
});
|
|
9985
10898
|
}
|
|
10899
|
+
function selectedIdentity(store, activeIdentity) {
|
|
10900
|
+
try {
|
|
10901
|
+
return store.resolveIdentityAlias(void 0, activeIdentity);
|
|
10902
|
+
} catch (error) {
|
|
10903
|
+
if (error instanceof AgentServerStoreError && error.code === "not_found") return;
|
|
10904
|
+
throw error;
|
|
10905
|
+
}
|
|
10906
|
+
}
|
|
10907
|
+
function identityViews(store) {
|
|
10908
|
+
const activated = new Set(store.listActivations().map((activation) => activation.alias));
|
|
10909
|
+
return store.listIdentityAliases().map((alias) => ({
|
|
10910
|
+
alias,
|
|
10911
|
+
activated: activated.has(alias),
|
|
10912
|
+
hasAgentKey: Boolean(store.readAgentConfig(alias)?.agent_key_ref)
|
|
10913
|
+
}));
|
|
10914
|
+
}
|
|
9986
10915
|
function registerProviderRoutes(app, options, requirePairedOrigin) {
|
|
9987
|
-
const { store } = options;
|
|
9988
|
-
let mutationQueue = Promise.resolve();
|
|
9989
|
-
const serialize = (mutation) => {
|
|
9990
|
-
const result = mutationQueue.then(mutation, mutation);
|
|
9991
|
-
mutationQueue = result.then(() => void 0, () => void 0);
|
|
9992
|
-
return result;
|
|
9993
|
-
};
|
|
9994
10916
|
app.get("/v1/providers", { schema: AgentServerRouteSchemas.listProviders }, async (request) => {
|
|
9995
10917
|
requirePairedOrigin(request);
|
|
9996
|
-
return
|
|
10918
|
+
return options.providers.list();
|
|
9997
10919
|
});
|
|
9998
10920
|
app.post("/v1/providers/:providerId/discover-models", { schema: AgentServerRouteSchemas.discoverModels }, async (request) => {
|
|
9999
10921
|
requirePairedOrigin(request);
|
|
10000
|
-
const { providerId
|
|
10001
|
-
|
|
10002
|
-
const provider = store.readProviders()[providerId];
|
|
10003
|
-
if (!provider) throw new AgentServerHttpError(404, "provider_not_found", `provider "${providerId}" was not found`);
|
|
10004
|
-
const parsed = parseProviderBaseUrl(provider.baseUrl, providerId);
|
|
10005
|
-
const baseUrl = parsed.href.replace(/\/$/u, "");
|
|
10006
|
-
let apiKey;
|
|
10007
|
-
if (provider.apiKeyRef) try {
|
|
10008
|
-
apiKey = await options.secretProviders.resolve(parseSecretReferenceString(provider.apiKeyRef));
|
|
10009
|
-
} catch (error) {
|
|
10010
|
-
request.log.warn({
|
|
10011
|
-
...safeErrorContext(error),
|
|
10012
|
-
code: "agent_server_provider_secret_unavailable",
|
|
10013
|
-
providerId
|
|
10014
|
-
}, "Provider API key could not be resolved for model discovery");
|
|
10015
|
-
throw new AgentServerHttpError(400, "provider_secret_unavailable", `provider "${providerId}" API key could not be resolved`);
|
|
10016
|
-
}
|
|
10017
|
-
const headers = apiKey ? { authorization: `Bearer ${apiKey}` } : {};
|
|
10018
|
-
const fetchImpl = options.discoverFetch ?? fetch;
|
|
10019
|
-
const failures = [];
|
|
10020
|
-
const collector = new ModelDiscoveryCollector();
|
|
10021
|
-
const tryJson = async (endpoint, url) => {
|
|
10022
|
-
let response;
|
|
10023
|
-
try {
|
|
10024
|
-
response = await fetchImpl(url, {
|
|
10025
|
-
headers,
|
|
10026
|
-
redirect: "error",
|
|
10027
|
-
signal: AbortSignal.timeout(1e4)
|
|
10028
|
-
});
|
|
10029
|
-
} catch (error) {
|
|
10030
|
-
const errorType = error instanceof Error ? error.name : typeof error;
|
|
10031
|
-
failures.push({
|
|
10032
|
-
kind: "network",
|
|
10033
|
-
errorType
|
|
10034
|
-
});
|
|
10035
|
-
request.log.warn({
|
|
10036
|
-
code: "agent_server_provider_discovery_request_failed",
|
|
10037
|
-
endpoint,
|
|
10038
|
-
errorType,
|
|
10039
|
-
providerId
|
|
10040
|
-
}, "Provider model discovery request failed");
|
|
10041
|
-
return null;
|
|
10042
|
-
}
|
|
10043
|
-
if (!response.ok) {
|
|
10044
|
-
failures.push({
|
|
10045
|
-
kind: "http",
|
|
10046
|
-
status: response.status
|
|
10047
|
-
});
|
|
10048
|
-
const context = {
|
|
10049
|
-
code: "agent_server_provider_discovery_upstream_error",
|
|
10050
|
-
endpoint,
|
|
10051
|
-
providerId,
|
|
10052
|
-
statusCode: response.status
|
|
10053
|
-
};
|
|
10054
|
-
if (response.status >= 500 || response.status === 401 || response.status === 403) request.log.warn(context, "Provider model discovery was rejected");
|
|
10055
|
-
else request.log.info(context, "Provider model discovery endpoint unavailable");
|
|
10056
|
-
return null;
|
|
10057
|
-
}
|
|
10058
|
-
try {
|
|
10059
|
-
return await response.json();
|
|
10060
|
-
} catch {
|
|
10061
|
-
failures.push({ kind: "invalid_response" });
|
|
10062
|
-
request.log.warn({
|
|
10063
|
-
code: "agent_server_provider_discovery_invalid_json",
|
|
10064
|
-
endpoint,
|
|
10065
|
-
providerId
|
|
10066
|
-
}, "Provider model discovery returned invalid JSON");
|
|
10067
|
-
return null;
|
|
10068
|
-
}
|
|
10069
|
-
};
|
|
10070
|
-
collector.addOpenAiResponse(await tryJson("openai_models", `${baseUrl}/models`));
|
|
10071
|
-
if (collector.size === 0) collector.addOllamaResponse(await tryJson("ollama_tags", `${parsed.origin}/api/tags`));
|
|
10072
|
-
const result = collector.result(providerId, failures);
|
|
10073
|
-
if (result.discoveredCount > result.models.length) request.log.warn({
|
|
10074
|
-
code: "agent_server_provider_discovery_truncated",
|
|
10075
|
-
discoveredCount: result.discoveredCount,
|
|
10076
|
-
providerId,
|
|
10077
|
-
returnedCount: 500
|
|
10078
|
-
}, "Provider model discovery result was truncated");
|
|
10079
|
-
request.log.info({
|
|
10080
|
-
code: "agent_server_provider_discovery_completed",
|
|
10081
|
-
modelCount: result.models.length,
|
|
10082
|
-
providerId
|
|
10083
|
-
}, "Provider model discovery completed");
|
|
10084
|
-
return { models: result.models };
|
|
10922
|
+
const { providerId } = request.params;
|
|
10923
|
+
return options.providers.discover(providerId, { signal: requestOperationSignal(request, options.shutdownSignal) });
|
|
10085
10924
|
});
|
|
10086
10925
|
app.put("/v1/providers/:providerId", {
|
|
10087
10926
|
schema: AgentServerRouteSchemas.putProvider,
|
|
10088
10927
|
attachValidation: true
|
|
10089
10928
|
}, async (request, reply) => {
|
|
10090
10929
|
requirePairedOrigin(request);
|
|
10091
|
-
const { providerId
|
|
10092
|
-
const providerId = assertProviderId(rawProviderId);
|
|
10930
|
+
const { providerId } = request.params;
|
|
10093
10931
|
const body = requireBody(request);
|
|
10094
|
-
const
|
|
10095
|
-
parseProviderBaseUrl(baseUrl, providerId);
|
|
10096
|
-
const entry = {
|
|
10932
|
+
const entry = await options.providers.set(providerId, {
|
|
10097
10933
|
api: requireString(body, "api"),
|
|
10098
|
-
baseUrl,
|
|
10099
|
-
envName:
|
|
10100
|
-
models: stringArray(body, "models", { allowEmpty: true })
|
|
10101
|
-
|
|
10102
|
-
const apiKey = optionalString(body, "apiKey");
|
|
10103
|
-
await serialize(async () => {
|
|
10104
|
-
const providers = store.readProviders();
|
|
10105
|
-
if (apiKey) {
|
|
10106
|
-
const key = `pi-provider/${providerId}`;
|
|
10107
|
-
await options.secrets.write(key, apiKey);
|
|
10108
|
-
entry.apiKeyRef = formatSecretReferenceString({
|
|
10109
|
-
provider: FILE_SECRET_PROVIDER,
|
|
10110
|
-
key
|
|
10111
|
-
});
|
|
10112
|
-
} else if (providers[providerId]?.apiKeyRef) entry.apiKeyRef = providers[providerId].apiKeyRef;
|
|
10113
|
-
providers[providerId] = entry;
|
|
10114
|
-
store.writeProviders(providers);
|
|
10934
|
+
baseUrl: requireString(body, "baseUrl"),
|
|
10935
|
+
envName: requireString(body, "envName"),
|
|
10936
|
+
models: stringArray(body, "models", { allowEmpty: true }),
|
|
10937
|
+
...optionalString(body, "apiKey") ? { apiKey: optionalString(body, "apiKey") } : {}
|
|
10115
10938
|
});
|
|
10116
|
-
return reply.code(200).send(
|
|
10939
|
+
return reply.code(200).send(entry);
|
|
10117
10940
|
});
|
|
10118
10941
|
app.delete("/v1/providers/:providerId", {
|
|
10119
10942
|
schema: AgentServerRouteSchemas.deleteProvider,
|
|
10120
10943
|
attachValidation: true
|
|
10121
10944
|
}, async (request, reply) => {
|
|
10122
10945
|
requirePairedOrigin(request);
|
|
10123
|
-
const { providerId
|
|
10124
|
-
|
|
10125
|
-
|
|
10126
|
-
|
|
10127
|
-
|
|
10128
|
-
|
|
10129
|
-
|
|
10130
|
-
delete providers[providerId];
|
|
10131
|
-
store.writeProviders(providers);
|
|
10132
|
-
});
|
|
10946
|
+
const { providerId } = request.params;
|
|
10947
|
+
try {
|
|
10948
|
+
await options.providers.remove(providerId);
|
|
10949
|
+
} catch (error) {
|
|
10950
|
+
if (error instanceof ProviderConfigurationError && error.code === "provider_not_found") throw new AgentServerHttpError(404, "agent_server_provider_not_found", error.message);
|
|
10951
|
+
throw error;
|
|
10952
|
+
}
|
|
10133
10953
|
return reply.code(204).send(null);
|
|
10134
10954
|
});
|
|
10135
10955
|
}
|
|
@@ -10278,40 +11098,6 @@ function registerRunLogRoute(app, options, requirePairedOrigin) {
|
|
|
10278
11098
|
return reply;
|
|
10279
11099
|
});
|
|
10280
11100
|
}
|
|
10281
|
-
function safeErrorContext(error) {
|
|
10282
|
-
const context = { errorType: error instanceof Error ? error.name : typeof error };
|
|
10283
|
-
const applicationCode = safeErrorToken(error?.code);
|
|
10284
|
-
if (applicationCode) context["applicationCode"] = applicationCode;
|
|
10285
|
-
const cause = error instanceof Error ? error.cause : void 0;
|
|
10286
|
-
if (cause instanceof Error) {
|
|
10287
|
-
context["causeType"] = cause.name;
|
|
10288
|
-
const causeMessage = safeLogMessage(cause.message);
|
|
10289
|
-
if (causeMessage) context["causeMessage"] = causeMessage;
|
|
10290
|
-
}
|
|
10291
|
-
const fsCode = safeErrorToken(cause?.code);
|
|
10292
|
-
const syscall = safeErrorToken(cause?.syscall);
|
|
10293
|
-
if (fsCode) context["fsCode"] = fsCode;
|
|
10294
|
-
if (syscall) context["syscall"] = syscall;
|
|
10295
|
-
const causeStatus = cause?.statusCode;
|
|
10296
|
-
if (typeof causeStatus === "number") context["causeStatusCode"] = causeStatus;
|
|
10297
|
-
return context;
|
|
10298
|
-
}
|
|
10299
|
-
function safeLogMessage(value) {
|
|
10300
|
-
const normalized = value.replace(/[\r\n\t]/gu, " ").trim();
|
|
10301
|
-
return normalized ? normalized.slice(0, 500) : void 0;
|
|
10302
|
-
}
|
|
10303
|
-
function safeErrorToken(value) {
|
|
10304
|
-
return typeof value === "string" && /^[a-z0-9_:-]{1,64}$/iu.test(value) ? value : void 0;
|
|
10305
|
-
}
|
|
10306
|
-
function providerView(provider) {
|
|
10307
|
-
return {
|
|
10308
|
-
api: provider.api,
|
|
10309
|
-
baseUrl: provider.baseUrl,
|
|
10310
|
-
envName: provider.envName,
|
|
10311
|
-
models: provider.models,
|
|
10312
|
-
hasApiKey: Boolean(provider.apiKeyRef)
|
|
10313
|
-
};
|
|
10314
|
-
}
|
|
10315
11101
|
function corsHeadersFor(request, options) {
|
|
10316
11102
|
const origin = request.headers.origin;
|
|
10317
11103
|
if (isConfiguredOrigin(origin, options)) return {
|
|
@@ -10359,6 +11145,11 @@ function normalizeAgentServerError(error) {
|
|
|
10359
11145
|
code: error.code,
|
|
10360
11146
|
message: error.message
|
|
10361
11147
|
};
|
|
11148
|
+
if (error instanceof ProviderConfigurationError) return {
|
|
11149
|
+
statusCode: error.statusCode,
|
|
11150
|
+
code: error.code,
|
|
11151
|
+
message: error.message
|
|
11152
|
+
};
|
|
10362
11153
|
if (error instanceof AgentServerIdentityError) return {
|
|
10363
11154
|
statusCode: error.code === "agent_exists" ? 409 : 400,
|
|
10364
11155
|
code: error.code,
|
|
@@ -10586,6 +11377,12 @@ async function runAgentServer(argv) {
|
|
|
10586
11377
|
authPath: store.piAuthJsonPath,
|
|
10587
11378
|
logger
|
|
10588
11379
|
});
|
|
11380
|
+
const providers = new ProviderConfigurationService({
|
|
11381
|
+
store,
|
|
11382
|
+
secrets,
|
|
11383
|
+
secretProviders,
|
|
11384
|
+
logger
|
|
11385
|
+
});
|
|
10589
11386
|
const runs = new RunManager({
|
|
10590
11387
|
store,
|
|
10591
11388
|
secretProviders,
|
|
@@ -10603,6 +11400,7 @@ async function runAgentServer(argv) {
|
|
|
10603
11400
|
pairing,
|
|
10604
11401
|
runs,
|
|
10605
11402
|
subscriptions,
|
|
11403
|
+
providers,
|
|
10606
11404
|
allowedOrigins,
|
|
10607
11405
|
selfOrigin: `${tls ? "https" : "http"}://127.0.0.1:${port}`,
|
|
10608
11406
|
...tls ? { tls: {
|
|
@@ -10610,6 +11408,7 @@ async function runAgentServer(argv) {
|
|
|
10610
11408
|
cert: tls.cert
|
|
10611
11409
|
} } : {},
|
|
10612
11410
|
defaultApiUrl,
|
|
11411
|
+
...envConfig.activeIdentity ? { activeIdentity: envConfig.activeIdentity } : {},
|
|
10613
11412
|
version: "dev",
|
|
10614
11413
|
logger,
|
|
10615
11414
|
shutdownSignal: shutdownController.signal
|
|
@@ -10681,7 +11480,7 @@ function waitForAgentServerShutdown(runs, app, shutdownController) {
|
|
|
10681
11480
|
const shutdown = () => {
|
|
10682
11481
|
if (shuttingDown) return;
|
|
10683
11482
|
shuttingDown = true;
|
|
10684
|
-
shutdownController.abort();
|
|
11483
|
+
shutdownController.abort({ source: "shutdown" });
|
|
10685
11484
|
(async () => {
|
|
10686
11485
|
app.server.closeAllConnections();
|
|
10687
11486
|
const cleanupPromise = Promise.allSettled([runs.stopAll(), app.close()]);
|
|
@@ -11053,7 +11852,7 @@ async function writeCache(cache) {
|
|
|
11053
11852
|
}
|
|
11054
11853
|
//#endregion
|
|
11055
11854
|
//#region src/version.ts
|
|
11056
|
-
var DAEMON_VERSION = "0.
|
|
11855
|
+
var DAEMON_VERSION = "0.56.0";
|
|
11057
11856
|
//#endregion
|
|
11058
11857
|
//#region src/cli.ts
|
|
11059
11858
|
async function runAgentDaemonCli(options) {
|
|
@@ -11073,6 +11872,7 @@ async function runAgentDaemonCli(options) {
|
|
|
11073
11872
|
case "once": return runOnce(rest, options.runtime);
|
|
11074
11873
|
case "drain": return runDrain(rest, options.runtime);
|
|
11075
11874
|
case "server": return runAgentServer(rest);
|
|
11875
|
+
case "providers": return runProviders(rest);
|
|
11076
11876
|
case "sync-sessions": return runSyncSessions(rest);
|
|
11077
11877
|
case "update": return runUpdate(rest);
|
|
11078
11878
|
case "runtime": return runRuntime(rest);
|