@aiwg/cli 2026.10.7 → 2026.10.9

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.
Files changed (33) hide show
  1. package/dist/src/artifacts/cli.js +9 -5
  2. package/dist/src/auth/config.js +3 -8
  3. package/dist/src/auth/resource-credentials.js +35 -2
  4. package/dist/src/cli/handlers/cost-report.js +7 -1
  5. package/dist/src/cli/handlers/decision.js +33 -8
  6. package/dist/src/cli/handlers/diagnose.js +13 -0
  7. package/dist/src/cli/handlers/local-executor.js +33 -6
  8. package/dist/src/cli/handlers/resource-versions.js +8 -5
  9. package/dist/src/cli/handlers/script-runner.js +13 -1
  10. package/dist/src/cli/handlers/use.js +18 -3
  11. package/dist/src/cost/fleet-report.js +20 -22
  12. package/dist/src/decision/driver.js +2 -2
  13. package/dist/src/decision/jev-credentials.js +18 -5
  14. package/dist/src/decision/ledger.js +230 -34
  15. package/dist/src/mcp/cli.mjs +105 -19
  16. package/dist/src/mcp/credentials.d.mts +4 -0
  17. package/dist/src/mcp/credentials.mjs +44 -3
  18. package/dist/src/mcp/grok-build-config.mjs +2 -0
  19. package/dist/src/mcp/omp-config.mjs +2 -0
  20. package/dist/src/mcp/registry.js +19 -15
  21. package/dist/src/mcp/registry.mjs +2 -18
  22. package/dist/src/mcp/tools/agentic-sandbox.mjs +46 -28
  23. package/dist/src/models/grok-build-models.js +20 -0
  24. package/dist/src/providers/provider-definitions.js +2 -2
  25. package/dist/src/resources/web-release.js +76 -65
  26. package/dist/src/serve/shared-host-scheduler.js +4 -1
  27. package/dist/src/storage/backends/fortemi.js +112 -50
  28. package/dist/src/storage/backends/logseq.js +16 -3
  29. package/dist/src/storage/backends/postgres.js +74 -33
  30. package/dist/src/storage/backends/postgrest.js +37 -13
  31. package/dist/src/storage/config.js +6 -1
  32. package/dist/src/uhp/config.js +4 -0
  33. package/package.json +1 -1
@@ -433,6 +433,9 @@ async function fetchBytes(fetcher, url, label, maxBytes, bearerToken) {
433
433
  catch (error) {
434
434
  if (timedOut)
435
435
  throw new Error(`${label} request timed out after ${RESOURCE_FETCH_TIMEOUT_MS}ms`);
436
+ const message = error instanceof Error ? error.message : String(error);
437
+ if (bearerToken && message.includes(bearerToken))
438
+ throw new Error(message.split(bearerToken).join('[REDACTED]'));
436
439
  throw error;
437
440
  }
438
441
  finally {
@@ -1072,80 +1075,88 @@ export async function resolveWebRelease(options = {}) {
1072
1075
  const headers = { ...(init.headers || {}), ...(bearerToken ? { authorization: `Bearer ${bearerToken}` } : {}) };
1073
1076
  return (options.fetcher ?? globalThis.fetch)(input, { ...init, headers });
1074
1077
  };
1075
- if (selector.kind === "exact") {
1076
- if (options.offline)
1077
- return resolveOfflineExact(cacheRoot, selector, selector.value, publicKeyPem, base);
1078
- const fetcher = authorize;
1079
- if (!fetcher)
1080
- throw new Error("No fetch implementation is available for AIWG web resources");
1081
- return fetchAndCacheRelease(base, fetcher, cacheRoot, selector, selector.value, publicKeyPem);
1082
- }
1083
- if (selector.kind === "range" || selector.kind === "digest") {
1084
- if (!options.offline && selector.kind === "digest") {
1085
- for (const version of cachedReleaseVersions(cacheRoot)) {
1086
- if (!cachedDigests(cacheRoot, version).includes(selector.digest))
1087
- continue;
1088
- try {
1089
- return verifyCachedGeneration(cacheRoot, version, selector.digest, selector, publicKeyPem, base, selector.digest);
1090
- }
1091
- catch {
1092
- // A corrupt immutable generation cannot bypass signed index resolution.
1078
+ try {
1079
+ if (selector.kind === "exact") {
1080
+ if (options.offline)
1081
+ return resolveOfflineExact(cacheRoot, selector, selector.value, publicKeyPem, base);
1082
+ const fetcher = authorize;
1083
+ if (!fetcher)
1084
+ throw new Error("No fetch implementation is available for AIWG web resources");
1085
+ return await fetchAndCacheRelease(base, fetcher, cacheRoot, selector, selector.value, publicKeyPem);
1086
+ }
1087
+ if (selector.kind === "range" || selector.kind === "digest") {
1088
+ if (!options.offline && selector.kind === "digest") {
1089
+ for (const version of cachedReleaseVersions(cacheRoot)) {
1090
+ if (!cachedDigests(cacheRoot, version).includes(selector.digest))
1091
+ continue;
1092
+ try {
1093
+ return verifyCachedGeneration(cacheRoot, version, selector.digest, selector, publicKeyPem, base, selector.digest);
1094
+ }
1095
+ catch {
1096
+ // A corrupt immutable generation cannot bypass signed index resolution.
1097
+ }
1093
1098
  }
1094
1099
  }
1100
+ const fetcher = authorize;
1101
+ const index = await resolveVersionIndex(base, fetcher, cacheRoot, publicKeyPem, options.offline, options.onDiagnostic);
1102
+ const selected = selectVersionFromIndex(index, selector);
1103
+ if (options.offline) {
1104
+ return resolveOfflineExact(cacheRoot, selector, selected.version, publicKeyPem, base, selected.releaseManifestSha256);
1105
+ }
1106
+ if (!fetcher)
1107
+ throw new Error("No fetch implementation is available for AIWG web resources");
1108
+ return await fetchAndCacheRelease(base, fetcher, cacheRoot, selector, selected.version, publicKeyPem, selected.releaseManifestSha256);
1095
1109
  }
1096
- const fetcher = authorize;
1097
- const index = await resolveVersionIndex(base, fetcher, cacheRoot, publicKeyPem, options.offline, options.onDiagnostic);
1098
- const selected = selectVersionFromIndex(index, selector);
1099
1110
  if (options.offline) {
1100
- return resolveOfflineExact(cacheRoot, selector, selected.version, publicKeyPem, base, selected.releaseManifestSha256);
1111
+ const cached = readCachedChannel(cacheRoot, selector.value, publicKeyPem);
1112
+ if (!cached)
1113
+ throw new Error(`AIWG resource channel ${selector.value} is not cached; offline mode cannot fetch it`);
1114
+ const release = resolveOfflineExact(cacheRoot, selector, cached.manifest.version, publicKeyPem, base, cached.manifest.releaseManifestSha256, cached.manifest.sequence);
1115
+ return cached.manifest.expiresAt ? { ...release, channelExpiresAt: cached.manifest.expiresAt } : release;
1101
1116
  }
1117
+ const fetcher = authorize;
1102
1118
  if (!fetcher)
1103
1119
  throw new Error("No fetch implementation is available for AIWG web resources");
1104
- return fetchAndCacheRelease(base, fetcher, cacheRoot, selector, selected.version, publicKeyPem, selected.releaseManifestSha256);
1105
- }
1106
- if (options.offline) {
1107
- const cached = readCachedChannel(cacheRoot, selector.value, publicKeyPem);
1108
- if (!cached)
1109
- throw new Error(`AIWG resource channel ${selector.value} is not cached; offline mode cannot fetch it`);
1110
- const release = resolveOfflineExact(cacheRoot, selector, cached.manifest.version, publicKeyPem, base, cached.manifest.releaseManifestSha256, cached.manifest.sequence);
1111
- return cached.manifest.expiresAt ? { ...release, channelExpiresAt: cached.manifest.expiresAt } : release;
1112
- }
1113
- const fetcher = authorize;
1114
- if (!fetcher)
1115
- throw new Error("No fetch implementation is available for AIWG web resources");
1116
- const channelPrefix = `resources/channels/${selector.value}`;
1117
- let prior = null;
1118
- try {
1119
- prior = readCachedChannel(cacheRoot, selector.value, publicKeyPem);
1120
+ const channelPrefix = `resources/channels/${selector.value}`;
1121
+ let prior = null;
1122
+ try {
1123
+ prior = readCachedChannel(cacheRoot, selector.value, publicKeyPem);
1124
+ }
1125
+ catch {
1126
+ prior = null;
1127
+ }
1128
+ const fetched = await fetchMetadata(fetcher, resourceUrl(base, `${channelPrefix}.json`), `channel ${selector.value}`, MAX_SIGNED_METADATA_BYTES, prior?.validator);
1129
+ if (fetched.notModified) {
1130
+ cacheChannel(cacheRoot, prior.manifest, prior.bytes, prior.signatureBytes, prior.digest, fetched.validator);
1131
+ options.onDiagnostic?.({ resource: "channel", outcome: "conditional-hit", validator: prior.validator.etag ? "etag" : "last-modified" });
1132
+ const release = await fetchAndCacheRelease(base, fetcher, cacheRoot, selector, prior.manifest.version, publicKeyPem, prior.manifest.releaseManifestSha256, prior.manifest.sequence);
1133
+ return prior.manifest.expiresAt ? { ...release, channelExpiresAt: prior.manifest.expiresAt } : release;
1134
+ }
1135
+ const channelBytes = fetched.bytes;
1136
+ const channelSignatureBytes = await fetchBytes(fetcher, resourceUrl(base, `${channelPrefix}.sig`), `channel ${selector.value} signature`, MAX_SIGNATURE_BYTES);
1137
+ const channelDigest = verifySignedResourceBytes(channelBytes, channelSignatureBytes, publicKeyPem, `channel ${selector.value}`);
1138
+ const channel = validateChannelManifest(parseJson(channelBytes, `channel ${selector.value}`), selector.value);
1139
+ if (prior && channel.sequence < prior.manifest.sequence) {
1140
+ throw new Error(`channel ${selector.value} sequence rollback detected (${channel.sequence} < ${prior.manifest.sequence})`);
1141
+ }
1142
+ if (prior &&
1143
+ channel.sequence === prior.manifest.sequence &&
1144
+ (channelDigest !== prior.digest ||
1145
+ channel.version !== prior.manifest.version ||
1146
+ channel.releaseManifestSha256 !== prior.manifest.releaseManifestSha256)) {
1147
+ throw new Error(`channel ${selector.value} sequence ${channel.sequence} has conflicting signed metadata`);
1148
+ }
1149
+ const release = await fetchAndCacheRelease(base, fetcher, cacheRoot, selector, channel.version, publicKeyPem, channel.releaseManifestSha256, channel.sequence);
1150
+ cacheChannel(cacheRoot, channel, channelBytes, channelSignatureBytes, channelDigest, fetched.validator);
1151
+ options.onDiagnostic?.({ resource: "channel", outcome: prior?.validator ? "revalidated" : "unconditional", validator: fetched.validator?.etag ? "etag" : fetched.validator?.lastModified ? "last-modified" : "none" });
1152
+ return channel.expiresAt ? { ...release, channelExpiresAt: channel.expiresAt } : release;
1120
1153
  }
1121
- catch {
1122
- prior = null;
1154
+ catch (error) {
1155
+ const message = error instanceof Error ? error.message : String(error);
1156
+ if (bearerToken && message.includes(bearerToken))
1157
+ throw new Error(message.split(bearerToken).join('[REDACTED]'));
1158
+ throw error;
1123
1159
  }
1124
- const fetched = await fetchMetadata(fetcher, resourceUrl(base, `${channelPrefix}.json`), `channel ${selector.value}`, MAX_SIGNED_METADATA_BYTES, prior?.validator);
1125
- if (fetched.notModified) {
1126
- cacheChannel(cacheRoot, prior.manifest, prior.bytes, prior.signatureBytes, prior.digest, fetched.validator);
1127
- options.onDiagnostic?.({ resource: "channel", outcome: "conditional-hit", validator: prior.validator.etag ? "etag" : "last-modified" });
1128
- const release = await fetchAndCacheRelease(base, fetcher, cacheRoot, selector, prior.manifest.version, publicKeyPem, prior.manifest.releaseManifestSha256, prior.manifest.sequence);
1129
- return prior.manifest.expiresAt ? { ...release, channelExpiresAt: prior.manifest.expiresAt } : release;
1130
- }
1131
- const channelBytes = fetched.bytes;
1132
- const channelSignatureBytes = await fetchBytes(fetcher, resourceUrl(base, `${channelPrefix}.sig`), `channel ${selector.value} signature`, MAX_SIGNATURE_BYTES);
1133
- const channelDigest = verifySignedResourceBytes(channelBytes, channelSignatureBytes, publicKeyPem, `channel ${selector.value}`);
1134
- const channel = validateChannelManifest(parseJson(channelBytes, `channel ${selector.value}`), selector.value);
1135
- if (prior && channel.sequence < prior.manifest.sequence) {
1136
- throw new Error(`channel ${selector.value} sequence rollback detected (${channel.sequence} < ${prior.manifest.sequence})`);
1137
- }
1138
- if (prior &&
1139
- channel.sequence === prior.manifest.sequence &&
1140
- (channelDigest !== prior.digest ||
1141
- channel.version !== prior.manifest.version ||
1142
- channel.releaseManifestSha256 !== prior.manifest.releaseManifestSha256)) {
1143
- throw new Error(`channel ${selector.value} sequence ${channel.sequence} has conflicting signed metadata`);
1144
- }
1145
- const release = await fetchAndCacheRelease(base, fetcher, cacheRoot, selector, channel.version, publicKeyPem, channel.releaseManifestSha256, channel.sequence);
1146
- cacheChannel(cacheRoot, channel, channelBytes, channelSignatureBytes, channelDigest, fetched.validator);
1147
- options.onDiagnostic?.({ resource: "channel", outcome: prior?.validator ? "revalidated" : "unconditional", validator: fetched.validator?.etag ? "etag" : fetched.validator?.lastModified ? "last-modified" : "none" });
1148
- return channel.expiresAt ? { ...release, channelExpiresAt: channel.expiresAt } : release;
1149
1160
  }
1150
1161
  export async function fetchVerifiedRawResource(release, resourcePath, options = {}) {
1151
1162
  assertSafeRelativePath(resourcePath, "raw resource path");
@@ -233,7 +233,10 @@ export class FileAdmissionStore {
233
233
  const file = openSync(this.path, constants.O_RDONLY | constants.O_NOFOLLOW);
234
234
  try {
235
235
  const stat = fstatSync(file);
236
- if (!stat.isFile() || stat.nlink !== 1 || (stat.mode & 0o077) !== 0 || stat.uid !== process.getuid?.() || stat.size > 16 * 1024 * 1024)
236
+ // nlink is 0 when a writer atomically renamed a new snapshot over the
237
+ // file after this reader opened it; the open descriptor is still a
238
+ // complete earlier snapshot. Only extra hard links are refused.
239
+ if (!stat.isFile() || stat.nlink > 1 || (stat.mode & 0o077) !== 0 || stat.uid !== process.getuid?.() || stat.size > 16 * 1024 * 1024)
237
240
  throw new Error('host authority must be a private bounded regular file');
238
241
  return JSON.parse(readFileSync(file, 'utf8'));
239
242
  }
@@ -37,6 +37,8 @@
37
37
  * @issue #972
38
38
  */
39
39
  import { createHash } from 'node:crypto';
40
+ import { debuglog } from 'node:util';
41
+ import { hasSecretLookingLiterals } from '../../mcp/credentials.mjs';
40
42
  const FORTEMI_QUERY_LIMIT = 50;
41
43
  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
42
44
  function schemaProperties(tool) {
@@ -81,7 +83,7 @@ export function resolveMcpRequestHeaders(server, environment = process.env) {
81
83
  const headers = { ...(server.headers ?? {}) };
82
84
  for (const [header, envName] of Object.entries(server.headerEnv ?? {})) {
83
85
  if (!ENV_NAME.test(envName)) {
84
- throw new Error(`storage(fortemi): invalid environment variable reference "${envName}"`);
86
+ throw new Error('storage(fortemi): invalid environment variable reference; use ENV_NAME, file:ABSOLUTE_PATH or store:NAME.');
85
87
  }
86
88
  const value = environment[envName];
87
89
  if (!value) {
@@ -371,8 +373,8 @@ export class FortemiAdapter {
371
373
  * Implemented as a lazy import so tests that inject a stub never load
372
374
  * the SDK or touch the registry.
373
375
  */
374
- export const createDefaultMcpClient = async (serverName, registryOverride, environment = process.env) => {
375
- const { McpServerRegistry } = await import('../../mcp/registry.js');
376
+ export const createDefaultMcpClient = async (serverName, registryOverride, environment = process.env, sensitiveInput = {}) => {
377
+ const { McpServerRegistry, resolveMcpCredentialMaps } = await import('../../mcp/registry.js');
376
378
  const registry = registryOverride ?? new McpServerRegistry();
377
379
  const server = await registry.get(serverName);
378
380
  if (!server) {
@@ -381,60 +383,120 @@ export const createDefaultMcpClient = async (serverName, registryOverride, envir
381
383
  }
382
384
  // Lazy imports keep unit tests that inject a stub isolated from transports.
383
385
  const { Client } = await import('@modelcontextprotocol/sdk/client/index.js');
384
- let transport;
385
- if (server.type === 'stdio') {
386
- const { StdioClientTransport } = await import('@modelcontextprotocol/sdk/client/stdio.js');
387
- transport = new StdioClientTransport({
388
- command: server.command ?? '',
389
- args: server.args ?? [],
390
- env: server.env,
391
- });
386
+ const hasCredentials = [server.env, server.headers, server.envFrom, server.headerEnv]
387
+ .some(map => Object.keys(map ?? {}).length > 0);
388
+ if (hasCredentials && server.type === 'stdio' && debuglog('child_process').enabled) {
389
+ throw new Error('MCP stdio credentials require NODE_DEBUG child_process logging to be disabled.');
392
390
  }
393
- else {
394
- if (!server.url) {
395
- throw new Error(`storage(fortemi): MCP server "${serverName}" has no URL`);
391
+ // Reuse the registry resolver for file/store references. Preserve legacy env
392
+ // bytes and Authorization's existing Bearer prefix at the transport boundary.
393
+ const hasNewReferences = [...Object.values(server.headerEnv ?? {}), ...Object.values(server.envFrom ?? {})]
394
+ .some(reference => /^(?:file|store):/.test(reference));
395
+ const resolved = hasNewReferences
396
+ ? await resolveMcpCredentialMaps(server, { ...sensitiveInput, environment })
397
+ : { env: server.env ? { ...server.env } : undefined, headers: resolveMcpRequestHeaders(server, environment) };
398
+ if (!hasNewReferences) {
399
+ for (const [key, name] of Object.entries(server.envFrom ?? {})) {
400
+ if (!ENV_NAME.test(name) || !environment[name]) {
401
+ throw new Error('storage(fortemi): envFrom requires a set ENV_NAME, file:ABSOLUTE_PATH or store:NAME.');
402
+ }
403
+ resolved.env = { ...resolved.env, [key]: environment[name] };
396
404
  }
397
- const url = validateRemoteMcpUrl(server.url);
398
- const headers = resolveMcpRequestHeaders(server, environment);
399
- if (server.type === 'http') {
400
- const { StreamableHTTPClientTransport } = await import('@modelcontextprotocol/sdk/client/streamableHttp.js');
401
- transport = new StreamableHTTPClientTransport(url, {
402
- requestInit: { headers },
403
- });
405
+ }
406
+ const secrets = [
407
+ ...Object.entries(resolved.env ?? {}).filter(([key, value]) => key in (server.envFrom ?? {}) || hasSecretLookingLiterals({ name: server.name, env: { [key]: value } })).map(([, value]) => value),
408
+ ...Object.values(resolved.headers ?? {}),
409
+ ...Object.values(resolved.headers ?? {}).map(value => value.replace(/^Bearer /i, '')),
410
+ ].filter(Boolean).sort((a, b) => b.length - a.length);
411
+ const redact = (text) => secrets.reduce((text, secret) => text.split(secret).join('[REDACTED]'), text);
412
+ const protect = async (operation) => {
413
+ try {
414
+ return await operation();
404
415
  }
405
- else if (server.type === 'sse') {
406
- const { SSEClientTransport } = await import('@modelcontextprotocol/sdk/client/sse.js');
407
- transport = new SSEClientTransport(url, {
408
- requestInit: { headers },
409
- eventSourceInit: {
410
- fetch: async (input, init) => {
411
- const merged = new Headers(init?.headers);
412
- for (const [name, value] of Object.entries(headers))
413
- merged.set(name, value);
414
- return fetch(input, { ...init, headers: merged });
415
- },
416
- },
416
+ catch (error) {
417
+ throw new Error(redact(error instanceof Error ? error.message : 'MCP transport failed'));
418
+ }
419
+ };
420
+ const protectResult = (value) => {
421
+ if (typeof value === 'string')
422
+ return redact(value);
423
+ if (Array.isArray(value))
424
+ return value.map(protectResult);
425
+ if (value && typeof value === 'object')
426
+ return Object.fromEntries(Object.entries(value).map(([key, value]) => [redact(key), protectResult(value)]));
427
+ return value;
428
+ };
429
+ try {
430
+ let transport;
431
+ if (server.type === 'stdio') {
432
+ const { StdioClientTransport } = await import('@modelcontextprotocol/sdk/client/stdio.js');
433
+ transport = new StdioClientTransport({
434
+ command: server.command ?? '',
435
+ args: server.args ?? [],
436
+ env: resolved.env,
437
+ ...(hasCredentials ? { stderr: 'pipe' } : {}),
417
438
  });
418
439
  }
419
440
  else {
420
- throw new Error(`storage(fortemi): unsupported MCP transport "${String(server.type)}"`);
441
+ if (!server.url) {
442
+ throw new Error(`storage(fortemi): MCP server "${serverName}" has no URL`);
443
+ }
444
+ const url = validateRemoteMcpUrl(server.url);
445
+ const headers = resolved.headers ?? {};
446
+ if (hasNewReferences) {
447
+ for (const header of Object.keys(server.headerEnv ?? {})) {
448
+ if (header.toLowerCase() === 'authorization')
449
+ headers[header] = `Bearer ${headers[header]}`;
450
+ }
451
+ }
452
+ if (server.type === 'http') {
453
+ const { StreamableHTTPClientTransport } = await import('@modelcontextprotocol/sdk/client/streamableHttp.js');
454
+ transport = new StreamableHTTPClientTransport(url, {
455
+ requestInit: { headers },
456
+ });
457
+ }
458
+ else if (server.type === 'sse') {
459
+ const { SSEClientTransport } = await import('@modelcontextprotocol/sdk/client/sse.js');
460
+ transport = new SSEClientTransport(url, {
461
+ requestInit: { headers },
462
+ eventSourceInit: {
463
+ fetch: async (input, init) => {
464
+ const merged = new Headers(init?.headers);
465
+ for (const [name, value] of Object.entries(headers))
466
+ merged.set(name, value);
467
+ return fetch(input, { ...init, headers: merged });
468
+ },
469
+ },
470
+ });
471
+ }
472
+ else {
473
+ throw new Error(`storage(fortemi): unsupported MCP transport "${String(server.type)}"`);
474
+ }
475
+ }
476
+ const client = new Client({ name: 'aiwg-storage-fortemi-adapter', version: '1.0.0' }, { capabilities: {} });
477
+ if (hasCredentials && server.type === 'stdio') {
478
+ // The recipient's stderr can contain credentials. Drain without forwarding.
479
+ transport.stderr?.on('data', () => { });
421
480
  }
481
+ await protect(() => client.connect(transport));
482
+ return {
483
+ async callTool(name, args) {
484
+ const call = async () => unwrapMcpToolResult(await client.callTool({ name, arguments: args }));
485
+ return protectResult(await protect(call));
486
+ },
487
+ async listTools() {
488
+ return protectResult(await protect(() => client.listTools()));
489
+ },
490
+ serverVersion() {
491
+ return protectResult(client.getServerVersion());
492
+ },
493
+ async close() {
494
+ await protect(() => client.close());
495
+ },
496
+ };
497
+ }
498
+ catch (error) {
499
+ throw new Error(redact(error instanceof Error ? error.message : 'MCP transport failed'));
422
500
  }
423
- const client = new Client({ name: 'aiwg-storage-fortemi-adapter', version: '1.0.0' }, { capabilities: {} });
424
- await client.connect(transport);
425
- return {
426
- async callTool(name, args) {
427
- return unwrapMcpToolResult(await client.callTool({ name, arguments: args }));
428
- },
429
- async listTools() {
430
- return client.listTools();
431
- },
432
- serverVersion() {
433
- return client.getServerVersion();
434
- },
435
- async close() {
436
- await client.close();
437
- },
438
- };
439
501
  };
440
502
  //# sourceMappingURL=fortemi.js.map
@@ -32,20 +32,26 @@
32
32
  * @issue #958
33
33
  */
34
34
  import { existsSync } from 'fs';
35
+ import { resolveSensitiveInput } from '../../auth/sensitive-input.js';
36
+ import { credentialReferenceInput } from '../../mcp/credentials.mjs';
35
37
  import { mkdir, readFile, readdir, rm, stat, writeFile } from 'fs/promises';
36
38
  import { dirname, join, relative, resolve, sep } from 'path';
37
39
  const LOGSEQ_CONFIG_DIR = 'logseq';
38
40
  const DEFAULT_API_URL = 'http://127.0.0.1:12315/api';
39
41
  export class LogseqAdapter {
42
+ sensitiveInput;
40
43
  /** Absolute path to the Logseq graph root. */
41
44
  graph;
42
45
  apiUrl;
43
46
  useApi;
47
+ apiTokenEnv;
44
48
  apiWarned = false;
45
- constructor(config) {
49
+ constructor(config, sensitiveInput = {}) {
50
+ this.sensitiveInput = sensitiveInput;
46
51
  this.graph = resolve(expandHome(config.graph));
47
52
  this.apiUrl = config.apiUrl ?? DEFAULT_API_URL;
48
53
  this.useApi = config.useApi ?? true;
54
+ this.apiTokenEnv = config.apiTokenEnv;
49
55
  }
50
56
  async init() {
51
57
  if (!existsSync(this.graph)) {
@@ -61,7 +67,14 @@ export class LogseqAdapter {
61
67
  async probeApi() {
62
68
  if (this.apiWarned)
63
69
  return;
64
- const token = process.env['LOGSEQ_API_TOKEN'];
70
+ const input = credentialReferenceInput(this.apiTokenEnv ?? 'LOGSEQ_API_TOKEN', 'Logseq apiTokenEnv');
71
+ // Keep the optional legacy environment path, including its missing-token warning.
72
+ const token = input.env
73
+ ? (this.sensitiveInput.environment ?? process.env)[input.env]
74
+ : await resolveSensitiveInput(input, this.sensitiveInput);
75
+ if (!token && this.apiTokenEnv !== undefined) {
76
+ throw new Error('storage(logseq): apiTokenEnv is not set; use ENV_NAME, file:ABSOLUTE_PATH or store:NAME.');
77
+ }
65
78
  if (!token) {
66
79
  this.apiWarned = true;
67
80
  process.stderr.write(`storage(logseq): API requested (useApi: true) but LOGSEQ_API_TOKEN is not set.\n` +
@@ -89,7 +102,7 @@ export class LogseqAdapter {
89
102
  }
90
103
  catch {
91
104
  this.apiWarned = true;
92
- process.stderr.write(`storage(logseq): API server at ${this.apiUrl} is not reachable.\n` +
105
+ process.stderr.write(`storage(logseq): API server is not reachable.\n` +
93
106
  ` AIWG will continue with direct file writes against the graph.\n` +
94
107
  ` Start Logseq with the API server enabled, or set "useApi": false to suppress.\n`);
95
108
  }
@@ -1,3 +1,5 @@
1
+ import { resolveSensitiveInput } from '../../auth/sensitive-input.js';
2
+ import { credentialReferenceInput } from '../../mcp/credentials.mjs';
1
3
  import { createHash } from 'node:crypto';
2
4
  import { loadFeaturePackage } from '../../features/runtime.js';
3
5
  import { STORAGE_BACKEND_CONTRACT, } from '../backend-contract.js';
@@ -27,6 +29,7 @@ export class PostgresBackendError extends Error {
27
29
  */
28
30
  export class PostgresStorageBackend {
29
31
  options;
32
+ sensitiveInput;
30
33
  descriptor = {
31
34
  contract: STORAGE_BACKEND_CONTRACT,
32
35
  backend: 'postgres-direct',
@@ -50,8 +53,9 @@ export class PostgresStorageBackend {
50
53
  statementTimeoutMs;
51
54
  lockTimeoutMs;
52
55
  idleTransactionTimeoutMs;
53
- constructor(options) {
56
+ constructor(options, sensitiveInput = {}) {
54
57
  this.options = options;
58
+ this.sensitiveInput = sensitiveInput;
55
59
  validateIdentity(options.tenant, 'tenant');
56
60
  validateIdentity(options.subsystem, 'subsystem');
57
61
  this.pool = options.pool;
@@ -69,7 +73,7 @@ export class PostgresStorageBackend {
69
73
  }
70
74
  async init() {
71
75
  if (!this.pool)
72
- this.pool = await createPool(this.options);
76
+ this.pool = await createPool(this.options, this.sensitiveInput);
73
77
  if (this.options.schemaMode !== 'migrate') {
74
78
  const schema = await this.runQuery('SELECT schema_version FROM aiwg_storage_schema WHERE singleton = true');
75
79
  if (Number(schema.rows[0]?.schema_version) !== 1) {
@@ -187,7 +191,7 @@ export class PostgresStorageBackend {
187
191
  }
188
192
  /** Hold an exported PostgreSQL snapshot open while parallel consumers page it. */
189
193
  async openSnapshotLease() {
190
- const exporter = await this.requiredPool().connect();
194
+ const exporter = await this.connect();
191
195
  let closed = false;
192
196
  try {
193
197
  await exporter.query('BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY');
@@ -208,8 +212,11 @@ export class PostgresStorageBackend {
208
212
  try {
209
213
  await exporter.query('COMMIT');
210
214
  }
215
+ catch (error) {
216
+ throw classifyPostgresError(error);
217
+ }
211
218
  finally {
212
- exporter.release(false);
219
+ releaseClient(exporter, false);
213
220
  }
214
221
  },
215
222
  };
@@ -219,7 +226,7 @@ export class PostgresStorageBackend {
219
226
  await exporter.query('ROLLBACK');
220
227
  }
221
228
  catch { /* preserve original error */ }
222
- exporter.release(true);
229
+ releaseClient(exporter, true);
223
230
  throw classifyPostgresError(error);
224
231
  }
225
232
  }
@@ -228,7 +235,7 @@ export class PostgresStorageBackend {
228
235
  throw new PostgresBackendError('AIWG_POSTGRES_SNAPSHOT_INVALID', 'exported snapshot identifier is malformed');
229
236
  }
230
237
  const boundedLimit = bounded(limit, 1000, 1, 10_000, 'limit');
231
- const client = await this.requiredPool().connect();
238
+ const client = await this.connect();
232
239
  let failed = false;
233
240
  try {
234
241
  await client.query('BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY');
@@ -252,7 +259,7 @@ export class PostgresStorageBackend {
252
259
  throw classifyPostgresError(error);
253
260
  }
254
261
  finally {
255
- client.release(failed);
262
+ releaseClient(client, failed);
256
263
  }
257
264
  }
258
265
  async changes(cursor) {
@@ -322,12 +329,17 @@ export class PostgresStorageBackend {
322
329
  });
323
330
  }
324
331
  async close() {
325
- if (this.ownsPool && this.pool)
326
- await this.pool.end();
332
+ try {
333
+ if (this.ownsPool && this.pool)
334
+ await this.pool.end();
335
+ }
336
+ catch (error) {
337
+ throw classifyPostgresError(error);
338
+ }
327
339
  this.pool = undefined;
328
340
  }
329
341
  async withTransaction(operation, isolation = 'SERIALIZABLE') {
330
- const client = await this.requiredPool().connect();
342
+ const client = await this.connect();
331
343
  let failed = false;
332
344
  try {
333
345
  await client.query(`BEGIN ISOLATION LEVEL ${isolation}`);
@@ -347,7 +359,7 @@ export class PostgresStorageBackend {
347
359
  throw classifyPostgresError(error);
348
360
  }
349
361
  finally {
350
- client.release(failed);
362
+ releaseClient(client, failed);
351
363
  }
352
364
  }
353
365
  assertMutationIdentity(mutation) {
@@ -356,6 +368,14 @@ export class PostgresStorageBackend {
356
368
  throw new PostgresBackendError('AIWG_POSTGRES_IDENTITY_MISMATCH', 'mutation identity is outside this backend tenant/subsystem');
357
369
  }
358
370
  }
371
+ async connect() {
372
+ try {
373
+ return await this.requiredPool().connect();
374
+ }
375
+ catch (error) {
376
+ throw classifyPostgresError(error);
377
+ }
378
+ }
359
379
  requiredPool() {
360
380
  if (!this.pool)
361
381
  throw new PostgresBackendError('AIWG_POSTGRES_NOT_INITIALIZED', 'call init() before using the PostgreSQL backend');
@@ -370,11 +390,18 @@ export class PostgresStorageBackend {
370
390
  }
371
391
  }
372
392
  }
373
- async function createPool(options) {
393
+ async function createPool(options, sensitiveInput) {
374
394
  const envName = options.connectionStringEnv ?? 'AIWG_POSTGRES_URL';
375
- const connectionString = process.env[envName];
395
+ const input = credentialReferenceInput(envName, 'PostgreSQL connectionStringEnv');
396
+ let connectionString;
397
+ try {
398
+ connectionString = input.env ? (sensitiveInput.environment ?? process.env)[input.env] : await resolveSensitiveInput(input, sensitiveInput);
399
+ }
400
+ catch (error) {
401
+ throw new PostgresBackendError('AIWG_POSTGRES_CREDENTIAL_UNAVAILABLE', error.message);
402
+ }
376
403
  if (!connectionString) {
377
- throw new PostgresBackendError('AIWG_POSTGRES_CREDENTIAL_UNAVAILABLE', `PostgreSQL connection locator ${envName} is not set`);
404
+ throw new PostgresBackendError('AIWG_POSTGRES_CREDENTIAL_UNAVAILABLE', 'PostgreSQL connectionStringEnv is not set; use ENV_NAME (default AIWG_POSTGRES_URL), file:ABSOLUTE_PATH or store:NAME.');
378
405
  }
379
406
  if (options.ssl === undefined) {
380
407
  throw new PostgresBackendError('AIWG_POSTGRES_TLS_REQUIRED', 'PostgreSQL remote connections require explicit ssl=require or ssl=verify-full');
@@ -382,24 +409,29 @@ async function createPool(options) {
382
409
  if (options.ssl === 'disable' && !isLoopbackPostgresUrl(connectionString)) {
383
410
  throw new PostgresBackendError('AIWG_POSTGRES_TLS_REQUIRED', 'ssl=disable is limited to explicit loopback development endpoints');
384
411
  }
385
- const pg = await loadFeaturePackage('pg');
386
- const defaultExport = pg.default;
387
- const Pool = (pg.Pool ?? defaultExport?.Pool);
388
- if (!Pool)
389
- throw new PostgresBackendError('AIWG_POSTGRES_DRIVER_INVALID', 'optional pg package does not export Pool');
390
- const pool = new Pool({
391
- connectionString,
392
- max: bounded(options.maxConnections, DEFAULT_POOL_MAX, 1, 100, 'maxConnections'),
393
- connectionTimeoutMillis: bounded(options.connectionTimeoutMs, DEFAULT_TIMEOUT_MS, 1, 600_000, 'connectionTimeoutMs'),
394
- statement_timeout: bounded(options.statementTimeoutMs, DEFAULT_TIMEOUT_MS, 1, 600_000, 'statementTimeoutMs'),
395
- application_name: options.applicationName ?? 'aiwg',
396
- ssl: options.ssl === 'disable' ? false : { rejectUnauthorized: options.ssl === 'verify-full' },
397
- });
398
- // pg emits idle-client failures at pool scope. Registering a listener keeps
399
- // server restarts from becoming uncaught process exceptions; failed clients
400
- // are evicted by pg and the next backend operation reconnects normally.
401
- pool.on?.('error', () => undefined);
402
- return pool;
412
+ try {
413
+ const pg = await loadFeaturePackage('pg');
414
+ const defaultExport = pg.default;
415
+ const Pool = (pg.Pool ?? defaultExport?.Pool);
416
+ if (!Pool)
417
+ throw new PostgresBackendError('AIWG_POSTGRES_DRIVER_INVALID', 'optional pg package does not export Pool');
418
+ const pool = new Pool({
419
+ connectionString,
420
+ max: bounded(options.maxConnections, DEFAULT_POOL_MAX, 1, 100, 'maxConnections'),
421
+ connectionTimeoutMillis: bounded(options.connectionTimeoutMs, DEFAULT_TIMEOUT_MS, 1, 600_000, 'connectionTimeoutMs'),
422
+ statement_timeout: bounded(options.statementTimeoutMs, DEFAULT_TIMEOUT_MS, 1, 600_000, 'statementTimeoutMs'),
423
+ application_name: options.applicationName ?? 'aiwg',
424
+ ssl: options.ssl === 'disable' ? false : { rejectUnauthorized: options.ssl === 'verify-full' },
425
+ });
426
+ // pg emits idle-client failures at pool scope. Registering a listener keeps
427
+ // server restarts from becoming uncaught process exceptions; failed clients
428
+ // are evicted by pg and the next backend operation reconnects normally.
429
+ pool.on?.('error', () => undefined);
430
+ return pool;
431
+ }
432
+ catch (error) {
433
+ throw classifyPostgresError(error);
434
+ }
403
435
  }
404
436
  function isLoopbackPostgresUrl(connectionString) {
405
437
  try {
@@ -468,6 +500,15 @@ function classifyPostgresError(error) {
468
500
  if (['40001', '40P01', '55P03', '57014', '57P01', '57P02', '57P03', '08000', '08003', '08006', '08001'].includes(code)) {
469
501
  return new PostgresBackendError('AIWG_POSTGRES_RETRYABLE', `retryable PostgreSQL failure (${code})`, true);
470
502
  }
471
- return error instanceof Error ? error : new Error(String(error));
503
+ // Drivers may echo the entire connection string, passwords or server detail.
504
+ return new PostgresBackendError('AIWG_POSTGRES_OPERATION_FAILED', 'PostgreSQL operation failed; check connectivity, credentials and server configuration.');
505
+ }
506
+ function releaseClient(client, failed) {
507
+ try {
508
+ client.release(failed);
509
+ }
510
+ catch (error) {
511
+ throw classifyPostgresError(error);
512
+ }
472
513
  }
473
514
  //# sourceMappingURL=postgres.js.map