Files
stack/packages/macp/src/credential-resolver.ts
Mos (Agent) 10689a30d2
Some checks failed
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/pr/ci Pipeline failed
feat: monorepo consolidation — forge pipeline, MACP protocol, framework plugin, profiles/guides/skills
Work packages completed:
- WP1: packages/forge — pipeline runner, stage adapter, board tasks, brief classifier,
  persona loader with project-level overrides. 89 tests, 95.62% coverage.
- WP2: packages/macp — credential resolver, gate runner, event emitter, protocol types.
  65 tests, 96.24% coverage. Full Python-to-TS port preserving all behavior.
- WP3: plugins/mosaic-framework — OC rails injection plugin (before_agent_start +
  subagent_spawning hooks for Mosaic contract enforcement).
- WP4: profiles/ (domains, tech-stacks, workflows), guides/ (17 docs),
  skills/ (5 universal skills), forge pipeline assets (48 markdown files).

Board deliberation: docs/reviews/consolidation-board-memo.md
Brief: briefs/monorepo-consolidation.md

Consolidates mosaic/stack (forge, MACP, bootstrap framework) into mosaic/mosaic-stack.
154 new tests total. Zero Python — all TypeScript/ESM.
2026-03-30 19:43:24 +00:00

237 lines
7.5 KiB
TypeScript

import { existsSync, readFileSync, statSync } from 'node:fs';
import { homedir } from 'node:os';
import { join, resolve } from 'node:path';
import { CredentialError } from './types.js';
import type { ProviderRegistry } from './types.js';
export const DEFAULT_CREDENTIALS_DIR = resolve(join(homedir(), '.config', 'mosaic', 'credentials'));
export const OC_CONFIG_PATH = join(homedir(), '.openclaw', 'openclaw.json');
export const REDACTED_MARKER = '__OPENCLAW_REDACTED__';
export const PROVIDER_REGISTRY: ProviderRegistry = {
anthropic: {
credential_file: 'anthropic.env',
env_var: 'ANTHROPIC_API_KEY',
oc_env_key: 'ANTHROPIC_API_KEY',
oc_provider_path: 'anthropic',
},
openai: {
credential_file: 'openai.env',
env_var: 'OPENAI_API_KEY',
oc_env_key: 'OPENAI_API_KEY',
oc_provider_path: 'openai',
},
zai: {
credential_file: 'zai.env',
env_var: 'ZAI_API_KEY',
oc_env_key: 'ZAI_API_KEY',
oc_provider_path: 'zai',
},
};
export function extractProvider(modelRef: string): string {
const provider = String(modelRef).trim().split('/')[0]?.trim().toLowerCase() ?? '';
if (!provider) {
throw new CredentialError(`Unable to resolve provider from model reference: '${modelRef}'`);
}
if (!(provider in PROVIDER_REGISTRY)) {
throw new CredentialError(`Unsupported credential provider: ${provider}`);
}
return provider;
}
export function parseDotenv(content: string): Record<string, string> {
const parsed: Record<string, string> = {};
for (const rawLine of content.split('\n')) {
const line = rawLine.trim();
if (!line || line.startsWith('#')) continue;
if (!line.includes('=')) continue;
const eqIdx = line.indexOf('=');
const key = line.slice(0, eqIdx).trim();
if (!key) continue;
let value = line.slice(eqIdx + 1).trim();
if (
value.length >= 2 &&
value[0] === value[value.length - 1] &&
(value[0] === '"' || value[0] === "'")
) {
value = value.slice(1, -1);
}
parsed[key] = value;
}
return parsed;
}
function loadCredentialFile(path: string): Record<string, string> {
if (!existsSync(path)) return {};
return parseDotenv(readFileSync(path, 'utf-8'));
}
export function stripJSON5Extensions(content: string): string {
const strings: string[] = [];
const MARKER = '\x00OCSTR';
// 1. Remove full-line comments
content = content.replace(/^\s*\/\/[^\n]*$/gm, '');
// 2. Protect single-quoted strings
content = content.replace(/'([^']*)'/g, (_m, g1: string) => {
const idx = strings.length;
strings.push(g1);
return `${MARKER}${idx}\x00`;
});
// 3. Protect double-quoted strings
content = content.replace(/"([^"]*)"/g, (_m, g1: string) => {
const idx = strings.length;
strings.push(g1);
return `${MARKER}${idx}\x00`;
});
// 4. Structural transforms — safe because strings are now placeholders
content = content.replace(/,\s*([}\]])/g, '$1');
content = content.replace(/\b(\w[\w-]*)\b(?=\s*:)/g, '"$1"');
// 5. Restore string values with proper JSON escaping
for (let i = 0; i < strings.length; i++) {
content = content.replace(`${MARKER}${i}\x00`, JSON.stringify(strings[i]!));
}
return content;
}
export interface PermissionCheckOptions {
ocConfigPath?: string;
}
export function checkOCConfigPermissions(path: string, opts?: { getuid?: () => number }): boolean {
if (!existsSync(path)) return false;
const stat = statSync(path);
const mode = stat.mode & 0o777;
if (mode & 0o077) {
// world/group readable — log warning (matches Python behavior)
}
const getuid = opts?.getuid ?? process.getuid?.bind(process);
if (getuid && stat.uid !== getuid()) {
return false;
}
return true;
}
export function isValidCredential(value: string): boolean {
const stripped = String(value).trim();
return stripped.length > 0 && stripped !== REDACTED_MARKER;
}
function loadOCConfigCredentials(
provider: string,
envVar: string,
ocConfigPath?: string,
): Record<string, string> {
const configPath = ocConfigPath ?? OC_CONFIG_PATH;
if (!existsSync(configPath)) return {};
try {
if (!checkOCConfigPermissions(configPath)) return {};
const rawContent = readFileSync(configPath, 'utf-8');
const config = JSON.parse(stripJSON5Extensions(rawContent)) as Record<string, unknown>;
const providerMeta = PROVIDER_REGISTRY[provider];
const ocEnvKey = providerMeta?.oc_env_key ?? envVar;
const envBlock = config['env'];
if (typeof envBlock === 'object' && envBlock !== null && !Array.isArray(envBlock)) {
const envValue = (envBlock as Record<string, unknown>)[ocEnvKey];
if (typeof envValue === 'string' && isValidCredential(envValue)) {
return { [envVar]: envValue.trim() };
}
}
const models = config['models'];
const providers =
typeof models === 'object' && models !== null && !Array.isArray(models)
? ((models as Record<string, unknown>)['providers'] as Record<string, unknown> | undefined)
: undefined;
const ocProviderPath = providerMeta?.oc_provider_path ?? provider;
if (typeof providers === 'object' && providers !== null && !Array.isArray(providers)) {
const providerConfig = providers[ocProviderPath];
if (
typeof providerConfig === 'object' &&
providerConfig !== null &&
!Array.isArray(providerConfig)
) {
const apiKey = (providerConfig as Record<string, unknown>)['apiKey'];
if (typeof apiKey === 'string' && isValidCredential(apiKey)) {
return { [envVar]: apiKey.trim() };
}
}
}
} catch {
return {};
}
return {};
}
function resolveTargetEnvVar(
provider: string,
taskConfig?: Record<string, unknown> | null,
): string {
const providerMeta = PROVIDER_REGISTRY[provider]!;
const rawCredentials =
typeof taskConfig === 'object' && taskConfig !== null
? (taskConfig['credentials'] as Record<string, unknown> | undefined)
: undefined;
const credentials =
typeof rawCredentials === 'object' && rawCredentials !== null ? rawCredentials : {};
const envVar = String(credentials['provider_key_env'] || providerMeta.env_var).trim();
if (!envVar) {
throw new CredentialError(`Invalid credential env var override for provider: ${provider}`);
}
return envVar;
}
export interface ResolveCredentialsOptions {
taskConfig?: Record<string, unknown> | null;
credentialsDir?: string;
ocConfigPath?: string;
}
export function resolveCredentials(
modelRef: string,
opts?: ResolveCredentialsOptions,
): Record<string, string> {
const provider = extractProvider(modelRef);
const providerMeta = PROVIDER_REGISTRY[provider]!;
const envVar = resolveTargetEnvVar(provider, opts?.taskConfig);
const credentialRoot = resolve(opts?.credentialsDir ?? DEFAULT_CREDENTIALS_DIR);
const credentialFile = join(credentialRoot, providerMeta.credential_file);
// 1. Mosaic credential file
const fileValues = loadCredentialFile(credentialFile);
const fileValue = (fileValues[envVar] ?? '').trim();
if (fileValue) {
return { [envVar]: fileValue };
}
// 2. OpenClaw config
const ocValues = loadOCConfigCredentials(provider, envVar, opts?.ocConfigPath);
if (Object.keys(ocValues).length > 0) {
return ocValues;
}
// 3. Ambient environment
const ambientValue = String(process.env[envVar] ?? '').trim();
if (ambientValue) {
return { [envVar]: ambientValue };
}
throw new CredentialError(
`Missing required credential ${envVar} for provider ${provider} ` +
`(checked ${credentialFile}, OC config, then ambient environment)`,
);
}