97 lines
5.9 KiB
JavaScript
97 lines
5.9 KiB
JavaScript
// Fixture-only execution resolution. Not a membership/registration authority.
|
|
import { validId, validateProvider, validateAccount, validateSettingsProfile,
|
|
validateHarnessManifest } from './records.mjs';
|
|
export class FixtureError extends Error {
|
|
constructor(code) { super(code); this.name = 'FixtureError'; this.code = code; }
|
|
}
|
|
export const refuse = code => { throw new FixtureError(code); };
|
|
const obj = v => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
export function exact(v, required, optional = []) {
|
|
if (!obj(v) || required.some(k => !Object.hasOwn(v, k)) ||
|
|
Object.keys(v).some(k => ![...required, ...optional].includes(k))) refuse('invalid-fixture-input');
|
|
}
|
|
function checkRegistry(result) {
|
|
if (!obj(result) || !Array.isArray(result.errors) || result.errors.length || !obj(result.entries)) refuse('invalid-registry');
|
|
const entries = result.entries;
|
|
for (const [name, validate] of [['providers', validateProvider], ['accounts', validateAccount],
|
|
['profiles', validateSettingsProfile], ['harnesses', validateHarnessManifest]]) {
|
|
if (!obj(entries[name])) refuse('invalid-registry');
|
|
for (const [key, record] of Object.entries(entries[name])) {
|
|
if (validate(record).length) refuse('invalid-registry');
|
|
if (key !== (name === 'accounts' ? `${record.provider}/${record.id}` : record.id)) refuse('invalid-registry');
|
|
}
|
|
}
|
|
return entries;
|
|
}
|
|
export function resolveFixtureExecution(result, request) {
|
|
// Clone immediately: later caller mutation cannot change this resolution.
|
|
let r, registry;
|
|
try { r = structuredClone(request); registry = structuredClone(result); }
|
|
catch { refuse('invalid-fixture-input'); }
|
|
const entries = checkRegistry(registry);
|
|
exact(r, ['fixtureOnly', 'agentId', 'projectId', 'workspaceId', 'sessionId', 'executionId', 'profile'], ['accounts', 'fork']);
|
|
if (r.fixtureOnly !== true) refuse('fixture-only');
|
|
for (const key of ['agentId', 'projectId', 'workspaceId', 'sessionId', 'executionId', 'profile'])
|
|
if (!validId(r[key])) refuse('invalid-scope');
|
|
if (!Object.hasOwn(entries.profiles, r.profile)) refuse('missing-profile');
|
|
const profile = entries.profiles[r.profile];
|
|
const overrides = r.accounts ?? {};
|
|
if (!obj(overrides)) refuse('invalid-selection');
|
|
const providers = new Set([...(profile.providers ?? []), ...profile.allowedAccounts.map(ref => ref.split('/')[0])]);
|
|
for (const key of Object.keys(overrides)) if (!providers.has(key)) refuse('provider-not-enrolled');
|
|
if (Object.hasOwn(r, 'fork')) {
|
|
exact(r.fork, ['sourceSessionId', 'accounts']);
|
|
if (!validId(r.fork.sourceSessionId) || r.fork.sourceSessionId === r.sessionId || !obj(r.fork.accounts)) refuse('invalid-fork-pin');
|
|
for (const id of Object.keys(r.fork.accounts)) if (!providers.has(id)) refuse('revoked-fork-pin');
|
|
}
|
|
const accounts = Object.create(null), models = { providers: Object.create(null) }, types = Object.create(null);
|
|
for (const id of [...providers].sort()) {
|
|
if (!Object.hasOwn(entries.providers, id)) refuse('missing-provider');
|
|
const provider = entries.providers[id], cfg = provider.harnesses.pi;
|
|
if (!cfg || !entries.harnesses.pi) refuse('unsupported-harness');
|
|
let ref;
|
|
if (r.fork) {
|
|
ref = r.fork.accounts[id];
|
|
if (Object.hasOwn(overrides, id) && overrides[id] !== ref) refuse('fork-account-change');
|
|
} else ref = Object.hasOwn(overrides, id) ? overrides[id] : profile.defaultAccounts?.[id];
|
|
if (ref === undefined) {
|
|
if (!provider.credentialTypes.includes('none')) refuse(r.fork ? 'missing-fork-pin' : 'missing-account-selection');
|
|
} else {
|
|
if (typeof ref !== 'string' || !profile.allowedAccounts.includes(ref) || ref.split('/')[0] !== id)
|
|
refuse('account-not-enrolled');
|
|
if (!Object.hasOwn(entries.accounts, ref)) refuse('missing-account');
|
|
const account = entries.accounts[ref];
|
|
if (account.provider !== id || !provider.credentialTypes.includes(account.type)) refuse('credential-type-not-supported');
|
|
accounts[id] = ref; types[id] = account.type;
|
|
}
|
|
if (provider.kind === 'native') {
|
|
// No aliasing to another provider's credential slot.
|
|
if (cfg.providerId !== id) refuse('provider-alias-unsupported');
|
|
if (profile.models?.[id]) refuse('native-model-enforcement-unavailable');
|
|
} else {
|
|
if (!['openai-completions', 'openai-responses', 'anthropic-messages', 'google-generative-ai'].includes(cfg.api)) refuse('unsupported-model-api');
|
|
const selected = profile.models?.[id] ?? cfg.models;
|
|
if (!selected.length || selected.some(m => !cfg.models.includes(m))) refuse('model-not-enrolled');
|
|
models.providers[id] = { api: cfg.api, baseUrl: cfg.baseUrl, models: selected.map(model => ({ id: model })) };
|
|
if (!ref || types[id] === 'none') models.providers[id].apiKey = 'FIXTURE_NONE';
|
|
}
|
|
}
|
|
const scope = Object.fromEntries(['agentId', 'projectId', 'workspaceId', 'sessionId', 'executionId'].map(k => [k, r[k]]));
|
|
return { fixtureOnly: true, scope, profile: r.profile, accounts, types, models,
|
|
fork: r.fork ? { sourceSessionId: r.fork.sourceSessionId, accounts: { ...accounts } } : null };
|
|
}
|
|
// Real keys/tokens are deliberately outside this slice's accepted input language.
|
|
const marker = v => typeof v === 'string' && /^FIXTURE_[A-Z0-9_-]{1,128}$/.test(v);
|
|
export function validateFixtureCredential(value, type) {
|
|
if (type === 'none') { if (value !== null) refuse('invalid-fixture-credential'); return null; }
|
|
if (type === 'api_key') {
|
|
exact(value, ['type', 'key']);
|
|
if (value.type !== type || !marker(value.key)) refuse('invalid-fixture-credential');
|
|
} else if (type === 'oauth') {
|
|
exact(value, ['type', 'access', 'refresh', 'expires']);
|
|
if (value.type !== type || !marker(value.access) || !marker(value.refresh) ||
|
|
!Number.isSafeInteger(value.expires) || value.expires < 0) refuse('invalid-fixture-credential');
|
|
} else refuse('unsupported-fixture-credential');
|
|
return structuredClone(value);
|
|
}
|