Files
2026-08-30 23:49:34 +00:00

1561 lines
68 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { existsSync, readFileSync, statSync } from 'node:fs';
import { resolve } from 'node:path';
import { inspect, format } from 'node:util';
import { assetrepair, assetstore, events, hub, wire, algo, verify } from '@evomap/evolver-core';
import { AuthError, HubClientError, HubFetch, HubUnreachableError, connectPublicHub, gepEnvelope, globalFetchLike, resolveHubUrl, } from '@evomap/evolver-adapter-public';
import { loadEnvFileFromEnv, proxyClientFromEnv } from '@evomap/evolver-mcp';
import { resolveAtpSenderId } from './atp.js';
import { storeRepairedAsset } from './repairedAssetStore.js';
import { resolveExplicitNodeCredentials, resolveIdentityHome } from './identityHome.js';
const REUSE_CONTRACT = 'reuse.v1';
const PUBLISH_CONTRACT = 'publish.v1';
const REVERSIBILITY = 'irreversible';
const OAUTH_AUTH_REQUIRED_MESSAGE = "Hub authentication required; run 'evolver login' and retry";
const LEGACY_AUTH_REQUIRED_MESSAGE = 'Hub node authentication failed; verify the configured node credentials and retry';
const PRIVATE_AUTH_REQUIRED_MESSAGE = 'Private Hub authentication failed; verify proxy enrollment, credentials, and hub mode';
const BUNDLE_REQUIRED_MESSAGE = 'publish requires Gene + Capsule bundle; pass --gene <id|path> --capsule <id|path>';
const MAX_ASSETS = 50;
const PUBLISH_USAGE = [
'usage: evolver publish --asset <gene_id_or_path> --asset <capsule_id_or_path> --json [--dry-run] [--repair] [--no-recipe]',
' evolver publish --gene <id_or_path> --capsule <id_or_path> --json [--dry-run] [--no-recipe]',
' evolver publish --gene <id_or_path> --auto-pair --json [--dry-run] [--no-recipe]',
].join('\n');
const ASSET_FLAGS = new Set(['--asset', '--gene', '--capsule', '--event']);
const STABLE_CONTRACT_REASONS = new Set([
'missing_id',
'cli_unavailable',
'auth_required',
'not_found',
'network_error',
'unsupported',
'internal_error',
'redaction_unavailable',
'leak_detected',
'schema_invalid',
'bundle_required',
'quality_gate_failed',
'gene_unproven',
'insufficient_credits',
'unsafe_validation_command',
]);
const HUB_METADATA_KEYS = new Set([
'credit_cost',
'gdi_score',
'success_rate',
'reuse_count',
'ranking_score',
'source_node_id',
'fetched_at',
'receipt',
'hub_receipt',
'already_purchased',
'_semantic_similarity',
'semantic_similarity',
'_search_score',
'search_score',
'_match_score',
'match_score',
'_retrieval_rank',
'retrieval_rank',
'original_asset_id',
]);
let machineJsonStdoutBypass;
class ContractError extends Error {
reason;
safeMessage;
constructor(reason, safeMessage) {
super(safeMessage);
this.reason = reason;
this.safeMessage = safeMessage;
this.name = 'ContractError';
}
}
export async function runReuseCommand(args, deps = {}) {
const out = deps.out ?? process.stdout;
const parsed = parseReuseArgs(args);
if (!parsed.ok || !parsed.assetId) {
return writeJson(out, reuseFailure(parsed.reason ?? 'missing_id', parsed.message ?? 'reuse requires --id <asset_id>'), 1, deps);
}
let runtimeDeps;
try {
runtimeDeps = { ...deps, env: loadContractEnv(deps) };
}
catch (err) {
const failure = classifyError(err, 'reuse');
return writeJson(out, reuseFailure(failure.reason, failure.message), 1, deps);
}
const write = (value, code) => writeJson(out, value, code, runtimeDeps);
const assetId = parsed.assetId;
return withMachineJsonConsole(Boolean(parsed.jsonOut), runtimeDeps, async () => {
try {
const fetcher = runtimeDeps.fetchAssetById
?? runtimeDeps.transport?.fetchAssetById
?? createDefaultTransport(runtimeDeps).fetchAssetById;
const asset = await fetcher(assetId);
if (!asset)
return write(reuseFailure('not_found', 'asset not found'), 1);
const cleaned = stripHubMetadata(asset);
const computedAssetId = wire.computeAssetId(cleaned);
const identityMatches = assetId.startsWith('sha256:')
? computedAssetId === assetId
: stringField(cleaned, 'id') === assetId && computedAssetId === cleaned.asset_id;
if (!identityMatches) {
return write(reuseFailure('internal_error', 'asset integrity verification failed'), 1);
}
const store = runtimeDeps.assetStore ?? new assetstore.LocalJsonlProvider(contractAssetsDir(runtimeDeps));
await assertNoLocalReuseIdConflict(cleaned, store);
const provenance = new assetstore.ProvenanceStore(storeBaseDir(store, runtimeDeps));
const stored = await assetstore.ingestUntrusted(store, provenance, cleaned, 'hub');
return write({
ok: true,
contract: REUSE_CONTRACT,
status: 'ok',
asset_id: stored.asset_id,
action: 'reused',
}, 0);
}
catch (err) {
const failure = classifyError(err, 'reuse');
return write(reuseFailure(failure.reason, failure.message), 1);
}
});
}
export async function runPublishCommand(args, deps = {}) {
const out = deps.out ?? process.stdout;
if (args.includes('--help') || args.includes('-h')) {
if (args.includes('--json')) {
return writeJson(out, {
ok: true,
contract: PUBLISH_CONTRACT,
mode: 'help',
usage: PUBLISH_USAGE,
}, 0, deps);
}
out.write(`${PUBLISH_USAGE}\n`);
return 0;
}
const parsed = parsePublishArgs(args);
if (!parsed.ok || !parsed.assetRefs) {
return writeJson(out, publishFailure(parsed.reason ?? 'bundle_required', parsed.message ?? publishReasonMessage('bundle_required'), { retryable: false }), 1, deps);
}
const assetRefs = parsed.assetRefs;
let runtimeDeps;
try {
const inheritedEnv = deps.env ?? process.env;
const runtimeEnv = loadContractEnv(deps);
runtimeDeps = { ...deps, env: preserveInheritedSecrets(runtimeEnv, inheritedEnv) };
}
catch (err) {
const failure = classifyError(err, 'publish', deps.env ?? process.env);
return writeJson(out, publishFailure(failure.reason, failure.message, {
retryable: failure.retryable,
mode: parsed.dryRun ? 'dry_run' : 'publish',
}), 1, deps);
}
const write = (value, code) => writeJson(out, value, code, runtimeDeps);
return withMachineJsonConsole(Boolean(parsed.jsonOut), runtimeDeps, async () => {
try {
const effectiveRefs = parsed.autoPair
? await autoPairPublishRefs(assetRefs, parsed.geneRef, runtimeDeps)
: assetRefs;
const bundle = await buildPublishBundle(effectiveRefs, runtimeDeps);
if (!bundle.ok) {
return write(publishFailure(bundle.reason, bundle.message, { retryable: false, gates: bundle.gates }), 1);
}
if (bundle.blockReasons.length > 0) {
if (parsed.dryRun)
return write(dryRunEnvelope(bundle), 0);
const reason = bundle.blockReasons[0] ?? 'internal_error';
const detailMessage = bundle.blockMessages?.[reason];
return write(publishFailure(reason, detailMessage ?? publishReasonMessage(reason), {
retryable: false,
mode: 'publish',
gates: bundle.gates,
assets: bundle.assets,
}), 1);
}
const transport = deps.transport ?? createDefaultTransport(runtimeDeps, {
composeRecipe: parsed.noRecipe !== true,
});
const validate = deps.validate ?? transport.validate;
// A field-level refusal is a defect in the RECORD, not a verdict on the work in it. Read the Hub's own
// `details[]`, mend what is mechanically derivable, and (only when asked) try once more — otherwise the
// envelope still carries the plan, so the operator can see the asset is salvageable instead of losing it.
const validationAttempt = await callHubWithRepair(validate, bundle.sanitized, parsed.repair === true);
const validation = validationAttempt.result;
let effectiveAssets = validationAttempt.assets;
const validationCredits = extractCredits(validation.body);
const validationUnavailable = transport.validationCapabilityOptional === true
&& isValidationCapabilityUnavailable(validation.body);
if (!validation.ok && !(validationUnavailable && !parsed.dryRun)) {
if (validationUnavailable) {
return write(publishFailure('unsupported', publishReasonMessage('unsupported'), {
retryable: false,
mode: 'dry_run',
gates: bundle.gates,
assets: bundle.assets,
detail: 'Private Hub validation is not configured',
}), 1);
}
const reason = publishReasonFromResponse(validation.status, validation.body);
const detail = hubDetailFromBody(validation.body);
if (parsed.dryRun && reason === 'quality_gate_failed') {
bundle.gates.quality = 'fail';
if (!bundle.blockReasons.includes('quality_gate_failed'))
bundle.blockReasons.push('quality_gate_failed');
return write(dryRunEnvelope(bundle, validationCredits, detail, validationAttempt), 0);
}
return write(publishFailure(reason, publishReasonMessage(reason, runtimeDeps.env), {
retryable: publishRetryable(reason),
mode: parsed.dryRun ? 'dry_run' : 'publish',
gates: parsed.dryRun ? bundle.gates : { ...bundle.gates, quality: 'fail' },
assets: bundle.assets,
...(detail ? { detail } : {}),
...(validationCredits ? { credits: validationCredits } : {}),
...repairEnvelopeField(validationAttempt),
}), 1);
}
if (parsed.dryRun)
return write(dryRunEnvelope(bundle, validationCredits, undefined, validationAttempt), 0);
const publish = deps.publish ?? transport.publish;
const publishAttempt = await callHubWithRepair(publish, effectiveAssets, parsed.repair === true);
const published = publishAttempt.result;
effectiveAssets = publishAttempt.assets;
const repairApplied = mergeRepairAttempts(validationAttempt, publishAttempt);
const publishCredits = extractCredits(published.body);
if (!published.ok) {
const reason = publishReasonFromResponse(published.status, published.body);
const detail = hubDetailFromBody(published.body);
return write(publishFailure(reason, publishReasonMessage(reason, runtimeDeps.env), {
retryable: publishRetryable(reason),
mode: 'publish',
gates: bundle.gates,
assets: bundle.assets,
...(detail ? { detail } : {}),
...(publishCredits ? { credits: publishCredits } : {}),
...repairEnvelopeField(repairApplied),
}), 1);
}
// The repaired records are what the network now holds. Persist them so the local library and the Hub do
// not silently diverge; a storage failure is reported, never fatal — the publish already happened.
const repairPersisted = repairApplied?.applied === true
? await persistRepairedBundle(effectiveAssets, bundle.sanitized, runtimeDeps)
: undefined;
const payload = payloadRecord(published.body);
const decision = stringField(payload, 'decision');
const hubReason = stringField(payload, 'reason');
const safetyCandidate = decision === 'quarantine' && hubReason === 'safety_candidate';
if (decision === 'quarantine' && !safetyCandidate) {
const detail = hubDetailFromBody(published.body);
return write(publishFailure('quality_gate_failed', publishReasonMessage('quality_gate_failed'), {
retryable: false,
mode: 'publish',
gates: { ...bundle.gates, quality: 'fail' },
assets: bundle.assets,
...(detail ? { detail } : {}),
...(publishCredits ? { credits: publishCredits } : {}),
}), 1);
}
const status = safetyCandidate ? 'queued' : normalizePublishStatus(published.body);
if (!status) {
return write(publishFailure('internal_error', 'Hub publish response missing lifecycle status', {
retryable: false,
mode: 'publish',
gates: bundle.gates,
assets: bundle.assets,
...(publishCredits ? { credits: publishCredits } : {}),
}), 1);
}
const receiptId = stringField(payload, 'receipt_id');
const bundleId = stringField(payload, 'bundle_id');
const publishedBundle = { ...bundle, sanitized: [...effectiveAssets] };
appendPublishedCapsuleCall(publishedBundle, deps, { status, receiptId, bundleId });
const recipeId = parsed.noRecipe || deps.publish
? undefined
: await composePublishedRecipe(transport, effectiveAssets);
return write({
ok: true,
contract: PUBLISH_CONTRACT,
mode: 'publish',
status,
reversibility: REVERSIBILITY,
...(receiptId ? { receipt_id: receiptId } : {}),
...(bundleId ? { bundle_id: bundleId } : {}),
...(recipeId ? { recipe_id: recipeId } : {}),
...(safetyCandidate ? { hub_reason: hubReason } : {}),
gates: bundle.gates,
// What the Hub actually holds — after a repair that is the mended record, not the one first submitted.
assets: repairApplied?.applied === true ? summarizePublishAssets(effectiveAssets) : bundle.assets,
...(publishCredits ? { credits: publishCredits } : {}),
...repairEnvelopeField(repairApplied, repairPersisted),
}, 0);
}
catch (err) {
const failure = classifyError(err, 'publish', runtimeDeps.env ?? process.env);
return write(publishFailure(failure.reason, failure.message, {
retryable: failure.retryable,
mode: parsed.dryRun ? 'dry_run' : 'publish',
}), 1);
}
});
}
async function callHubWithRepair(call, assets, allowRepair) {
const first = await call(assets);
if (first.ok)
return { result: first, assets, applied: false, entries: [] };
const plan = planBundleRepair(assets, first.body);
// Retry only when the WHOLE bundle is mended. One unrepairable asset means the Hub will refuse the bundle
// again for the same reason, and a second refusal costs the operator another paid round-trip for nothing.
const repairable = plan.entries.some((entry) => entry.repair_status === 'repaired')
&& plan.entries.every((entry) => entry.repair_status !== 'unrepairable');
if (!repairable || !allowRepair)
return { result: first, assets, applied: false, entries: plan.entries };
// Exactly one retry: a Hub that refuses the mended record too is telling us the defect is not mechanical,
// and a repair loop against a paid endpoint is how you burn credits without converging.
const retried = await call(plan.assets);
return { result: retried, assets: plan.assets, applied: true, entries: plan.entries };
}
function planBundleRepair(assets, rejectionBody) {
const hubIssues = assetrepair.hubRejectionIssues(rejectionBody, {
assetTypes: assets.map((asset) => (typeof asset.type === 'string' ? asset.type : undefined)),
});
const bundleIssues = hubIssues.byAssetIndex.get(-1) ?? [];
const repaired = [];
const entries = [];
for (const [index, asset] of assets.entries()) {
const issues = [...(hubIssues.byAssetIndex.get(index) ?? []), ...bundleIssues];
const report = assetrepair.repairAssetRecord(asset, { hubIssues: issues });
repaired.push(report.asset ?? asset);
entries.push({
...(asset.asset_id ? { asset_id: asset.asset_id } : {}),
...(report.asset && report.asset.asset_id !== asset.asset_id ? { repaired_asset_id: report.asset.asset_id } : {}),
repair_status: report.status,
changes: report.changes.map((change) => `${change.path}: ${change.action}`),
blockers: report.blockers.map(repairBlockerText),
});
}
return { assets: repaired, entries };
}
function repairBlockerText(blocker) {
return `${blocker.path || '(record)'}: ${stripControlChars(blocker.message).replace(/\s+/g, ' ').trim()}`.slice(0, HUB_DETAIL_MAX_CHARS);
}
/** Report the LATER attempt's plan: it is the one describing the bundle that was actually refused last. */
function mergeRepairAttempts(validation, publish) {
if (publish.applied || publish.entries.length > 0) {
return { ...publish, applied: publish.applied || validation.applied };
}
return validation.entries.length > 0 || validation.applied ? validation : undefined;
}
function repairEnvelopeField(attempt, persisted) {
if (!attempt || (attempt.entries.length === 0 && !attempt.applied))
return {};
return {
repair: {
applied: attempt.applied,
...(persisted !== undefined ? { persisted } : {}),
...(attempt.applied ? {} : { hint: 're-run with --repair to publish the mended record' }),
assets: attempt.entries,
},
};
}
async function persistRepairedBundle(published, submitted, deps) {
const env = deps.env ?? process.env;
const store = deps.assetStore ?? new assetstore.LocalJsonlProvider(contractAssetsDir(deps));
// Only the records repair actually changed are new locally; the untouched ones are already stored.
const changed = published.filter((asset, index) => asset.asset_id !== submitted[index]?.asset_id);
try {
const stored = await Promise.all(changed.map((asset) => storeRepairedAsset(asset, store, env)));
return stored.every(Boolean);
}
catch {
return false;
}
}
function appendPublishedCapsuleCall(bundle, deps, receipt) {
const capsuleIndex = bundle.sanitized.findIndex((asset) => asset.type === 'Capsule');
if (capsuleIndex < 0)
return;
const publishedCapsule = bundle.sanitized[capsuleIndex];
const originalCapsule = bundle.original[capsuleIndex];
if (!publishedCapsule)
return;
const tokensSpent = hub.assetDerivationTokenCost(originalCapsule);
const trigger = Array.isArray(publishedCapsule['trigger'])
? publishedCapsule['trigger'].filter((value) => typeof value === 'string')
: undefined;
try {
const callLog = deps.callLog ?? new hub.AssetCallLog(events.assetCallLogPath(deps.env ?? process.env));
callLog.append({
action: 'asset_publish',
asset_id: publishedCapsule.asset_id,
asset_type: 'Capsule',
...(trigger ? { signals: trigger } : {}),
...(tokensSpent !== undefined ? { tokens_spent: tokensSpent } : {}),
extra: {
status: receipt.status,
...(receipt.receiptId ? { receipt_id: receipt.receiptId } : {}),
...(receipt.bundleId ? { bundle_id: receipt.bundleId } : {}),
},
});
}
catch { /* a local audit failure must never change an accepted Hub publish */ }
}
export function parseReuseArgs(args) {
let id;
let jsonOut = false;
for (let i = 0; i < args.length; i++) {
const token = args[i];
if (!token)
continue;
if (token === '--json') {
jsonOut = true;
continue;
}
if (token === '--id') {
const next = args[i + 1];
if (!next || next.startsWith('--'))
return { ok: false, reason: 'missing_id', message: 'reuse requires --id <asset_id>' };
id = next.trim();
i++;
continue;
}
if (token.startsWith('--id=')) {
id = token.slice('--id='.length).trim();
continue;
}
return { ok: false, reason: 'unsupported', message: 'unsupported reuse argument' };
}
if (!jsonOut)
return { ok: false, reason: 'unsupported', message: 'reuse requires --json' };
if (!id)
return { ok: false, reason: 'missing_id', message: 'reuse requires --id <asset_id>' };
if (id.length > 200)
return { ok: false, reason: 'missing_id', message: 'asset id must be <= 200 characters' };
return { ok: true, assetId: id, jsonOut };
}
export function parsePublishArgs(args) {
const assetRefs = [];
let geneRef;
let geneCount = 0;
let capsuleRef;
let hasUntypedAsset = false;
let autoPair = false;
let dryRun = false;
let repair = false;
let noRecipe = false;
let jsonOut = false;
for (let i = 0; i < args.length; i++) {
const token = args[i];
if (!token)
continue;
if (token === '--dry-run') {
dryRun = true;
continue;
}
if (token === '--json') {
jsonOut = true;
continue;
}
if (token === '--auto-pair') {
autoPair = true;
continue;
}
if (token === '--repair') {
repair = true;
continue;
}
if (token === '--no-recipe') {
noRecipe = true;
continue;
}
const equalFlag = [...ASSET_FLAGS].find((flag) => token.startsWith(`${flag}=`));
if (equalFlag) {
const value = token.slice(equalFlag.length + 1).trim();
if (!value)
return { ok: false, reason: 'bundle_required', message: `${equalFlag} requires a value` };
assetRefs.push(value);
if (equalFlag === '--gene') {
geneRef = value;
geneCount++;
}
if (equalFlag === '--capsule')
capsuleRef = value;
if (equalFlag === '--asset')
hasUntypedAsset = true;
continue;
}
if (ASSET_FLAGS.has(token)) {
const next = args[i + 1];
if (!next || next.startsWith('--'))
return { ok: false, reason: 'bundle_required', message: `${token} requires a value` };
const value = next.trim();
if (!value)
return { ok: false, reason: 'bundle_required', message: `${token} requires a value` };
assetRefs.push(value);
if (token === '--gene') {
geneRef = value;
geneCount++;
}
if (token === '--capsule')
capsuleRef = value;
if (token === '--asset')
hasUntypedAsset = true;
i++;
continue;
}
if (!token.startsWith('--'))
return { ok: false, reason: 'unsupported', message: 'unsupported publish argument; did you mean --asset <id|path>?' };
if (token === '--home' || token.startsWith('--home=') || token === '--evomap-home' || token.startsWith('--evomap-home=')) {
return { ok: false, reason: 'unsupported', message: 'publish does not accept home flags; set EVOLVER_HOME or EVOMAP_HOME before running evolver publish' };
}
return { ok: false, reason: 'unsupported', message: "unsupported publish flag; run 'evolver publish --help'" };
}
if (!jsonOut)
return { ok: false, reason: 'unsupported', message: 'publish requires --json' };
if (autoPair && (geneCount !== 1 || !geneRef || capsuleRef || hasUntypedAsset)) {
return { ok: false, reason: 'bundle_required', message: '--auto-pair requires exactly one explicit --gene and no --asset or --capsule' };
}
const refs = assetRefs;
if (refs.length === 0)
return { ok: false, reason: 'bundle_required', message: 'publish requires --asset <id|path>' };
if (refs.length > MAX_ASSETS)
return { ok: false, reason: 'bundle_required', message: `publish supports at most ${MAX_ASSETS} assets` };
return {
ok: true,
assetRefs: refs,
...(geneRef ? { geneRef } : {}),
...(autoPair ? { autoPair: true } : {}),
dryRun,
repair,
...(noRecipe ? { noRecipe: true } : {}),
jsonOut,
};
}
export async function buildPublishBundle(refs, deps = {}) {
let original;
try {
original = await Promise.all(refs.map((ref) => loadAssetRef(ref, deps)));
}
catch (err) {
// Report what actually went wrong. Overwriting every failure with "asset
// schema is invalid" hid the common case — a ref that resolved to nothing —
// behind a claim about a schema that was never read, leaving a user who
// could not publish with no way to tell a missing asset from a bad one.
const reason = err instanceof ContractError ? err.reason : 'internal_error';
const message = err instanceof ContractError ? err.safeMessage : publishReasonMessage(reason);
return {
ok: false,
reason,
message,
gates: reason === 'schema_invalid' ? { schema: 'fail' } : {},
};
}
const bundleCheck = checkBundle(original);
if (!bundleCheck.ok)
return { ok: false, reason: 'bundle_required', message: bundleCheck.message, gates: { schema: 'pass', bundle: 'fail' } };
let sanitized;
try {
sanitized = sanitizePublishBundle(original);
}
catch {
return { ok: false, reason: 'redaction_unavailable', message: 'redaction unavailable', gates: { redaction: 'unavailable' } };
}
const finalBundleCheck = checkBundle(sanitized);
if (!finalBundleCheck.ok) {
return { ok: false, reason: 'bundle_required', message: finalBundleCheck.message, gates: { redaction: 'pass', schema: 'pass', bundle: 'fail' } };
}
const leak = finalPayloadLeakCheck(sanitized, deps.env ?? process.env);
// Local quality gate: a gene may only publish once it has TRULY succeeded at
// least once. Minting sets `confidence` from self-report / text completeness,
// which never proves the gene worked; without this check an unproven gene
// leaks to the Hub and comes back as an opaque `quality_gate_failed` after a
// round-trip. Assessing the gene's outcome evidence here fails fast, locally,
// with a precise reason. Read-only: aggregates the local capsule history.
const geneEvidence = await assessPublishGeneEvidence(original, deps);
const geneProven = geneEvidence?.eligible ?? true;
// Local quality gate: validation command safety check.
// Only applies to Gene assets. Checks that all validation commands are safe
// (e.g., no `node -e`, no shell metacharacters).
const validationCommandResult = checkValidationCommands(original);
const gates = {
redaction: 'pass',
leak: leak.blocked ? 'fail' : 'pass',
schema: 'pass',
bundle: 'pass',
quality: geneProven ? 'pass' : 'fail',
quality_evidence: geneEvidence?.source ?? 'unavailable',
validation_command: validationCommandResult.allowed ? 'pass' : 'fail',
};
const blockReasons = [];
const blockMessages = {};
if (leak.blocked)
blockReasons.push('leak_detected');
if (!geneProven)
blockReasons.push('gene_unproven');
if (!validationCommandResult.allowed) {
blockReasons.push('unsafe_validation_command');
if (validationCommandResult.message) {
blockMessages['unsafe_validation_command'] = validationCommandResult.message;
}
}
return {
ok: true,
original,
sanitized,
blockReasons,
blockMessages,
gates,
assets: summarizePublishAssets(sanitized),
};
}
// 评估 bundle 中 Gene 的成功证据。历史记录来自本地 Capsule store显式提交的、
// 通过完整校验的成功 Capsule 也可作为首次发布的 seed。没有 gene id 或 store
// 读取失败时保持 fail-open因为这道预检只增加本地门槛最终质量判定仍由 Hub 负责。
async function assessPublishGeneEvidence(original, deps) {
const gene = original.find((asset) => asset.type === 'Gene');
if (!gene)
return null;
const businessId = stringField(gene, 'id');
const assetId = typeof gene.asset_id === 'string' ? gene.asset_id : undefined;
const geneId = businessId ?? assetId;
if (!geneId)
return null;
const geneIds = new Set([businessId, assetId].filter((id) => Boolean(id)));
const hasBundleSeed = hasVerifiedSuccessfulBundleCapsule(original, geneIds);
try {
const store = deps.assetStore ?? new assetstore.LocalJsonlProvider(contractAssetsDir(deps));
const primary = await algo.assessGenePublishEvidence(store, geneId);
// Capsules key their `gene` link by the gene's business id in production;
// some stores (and legacy fixtures) key it by asset_id. Mixed histories can
// contain failures under the business id and a success under asset_id, so
// both aliases must contribute to the gate rather than only falling back
// when the primary alias has no rows at all.
const byAssetId = assetId && assetId !== geneId
? await algo.assessGenePublishEvidence(store, assetId)
: undefined;
const localEvidence = {
success: primary.success + (byAssetId?.success ?? 0),
inert: primary.inert + (byAssetId?.inert ?? 0),
};
if (algo.isGenePublishEligible(localEvidence))
return { eligible: true, source: 'local_history' };
if (hasBundleSeed)
return { eligible: true, source: 'bundle_seed' };
return { eligible: false, source: 'none' };
}
catch {
// 本地 store 不可用时保持历史 fail-open 行为。显式 Capsule 只有在上面的
// content hash 和 wire schema 校验都通过后才会计入,非法文件不会变成证据。
if (hasBundleSeed)
return { eligible: true, source: 'bundle_seed' };
return null;
}
}
/**
* 首次发布可以把已经生成的成功 Capsule 作为同一 bundle 的证据提交。
* 之前这里只读取本地 capsules.jsonl导致 `--gene file --capsule file`
* 即使带有成功 Capsule 也在到达 Hub 前被 `gene_unproven` 拦截。
*
* 外部文件不是本地历史,必须先通过完整 wire schema 和 content hash 校验;
* 失败或缺少成功 outcome 的 Capsule 不会解锁 quality gate。该证据只用于
* 本地预检Hub 仍然负责最终质量判定和持久化。
*/
function hasVerifiedSuccessfulBundleCapsule(original, geneIds) {
const capsule = original.find((asset) => (asset.type === 'Capsule' && geneIds.has(stringField(asset, 'gene') ?? '')));
if (!capsule || !wire.validateWireDeep(capsule).ok || !wire.verifyAssetId(capsule)) {
return false;
}
return stringField(asRecord(capsule['outcome']), 'status') === 'success';
}
/**
* 检查 Gene 资产中的验证命令是否安全。
* 只检查 Gene 类型的资产,不影响 Capsule 或 EvolutionEvent。
* 如果 Gene 有 validation 数组,会检查每条命令是否符合安全策略。
* 不安全的命令(如 `node -e`、包含 shell 元字符等)会阻止发布。
*/
function checkValidationCommands(original) {
const gene = original.find((asset) => asset.type === 'Gene');
if (!gene)
return { allowed: true, unsafeCommands: [] };
const validation = gene['validation'];
if (!Array.isArray(validation) || validation.length === 0) {
return { allowed: true, unsafeCommands: [] };
}
const unsafeCommands = [];
for (const cmd of validation) {
if (typeof cmd !== 'string')
continue;
if (!verify.isValidationCommandAllowed(cmd)) {
unsafeCommands.push(cmd);
}
}
if (unsafeCommands.length === 0) {
return { allowed: true, unsafeCommands: [] };
}
// 构建诊断信息(不暴露原始命令文本,避免泄露安全策略细节)
const reasons = new Set();
for (const cmd of unsafeCommands) {
if (/`|\$\(/.test(cmd)) {
reasons.add('command_substitution');
continue;
}
const stripped = cmd.replace(/"[^"]*"/g, '').replace(/'[^']*'/g, '');
if (verify.SHELL_METACHARS.test(stripped)) {
reasons.add('shell_metacharacters');
continue;
}
const tokens = cmd.split(/\s+/);
const executable = tokens[0] ?? '';
const args = tokens.slice(1);
const badFlag = verify.isNodeExecutable(executable) ? verify.nodeFlagViolation(executable, args) : null;
if (badFlag) {
reasons.add('blocked_node_flag');
continue;
}
if (!cmd.trim().startsWith('node ')) {
reasons.add('non_node_command');
}
else {
reasons.add('unsafe_pattern');
}
}
return {
allowed: false,
unsafeCommands,
message: `validation command rejected: ${[...reasons].join(', ')}`,
};
}
function sanitizePublishBundle(original) {
const sanitized = original.map((asset) => hub.sanitizeAsset(stripPublishMetadata(asset)));
const finalGeneIdByOriginalRef = new Map();
for (let i = 0; i < original.length; i++) {
const source = original[i];
const finalAsset = sanitized[i];
if (source?.type !== 'Gene' || finalAsset?.type !== 'Gene')
continue;
for (const ref of [source.asset_id, stringField(source, 'id')]) {
if (ref)
finalGeneIdByOriginalRef.set(ref, finalAsset.asset_id);
}
}
return sanitized.map((asset, index) => {
if (asset.type !== 'Capsule')
return asset;
const originalGeneRef = stringField(original[index], 'gene');
const sanitizedGeneRef = stringField(asset, 'gene');
const finalGeneId = (originalGeneRef ? finalGeneIdByOriginalRef.get(originalGeneRef) : undefined)
?? (sanitizedGeneRef ? finalGeneIdByOriginalRef.get(sanitizedGeneRef) : undefined);
if (!finalGeneId || sanitizedGeneRef === finalGeneId)
return asset;
return withRecomputedAssetId({ ...asset, gene: finalGeneId });
});
}
function withRecomputedAssetId(asset) {
const assetId = wire.computeAssetId(asset);
return { ...asset, asset_id: assetId ?? asset.asset_id };
}
/** Resolve `<asset_id|logical_id|path>` the way publish does. Shared so `asset-repair` speaks the same refs. */
export async function loadAssetRef(ref, deps) {
if (looksLikeFile(ref)) {
try {
return normalizeAsset(JSON.parse(readFileSync(resolve(ref), 'utf8')));
}
catch (err) {
if (err instanceof ContractError)
throw err;
if (err instanceof SyntaxError)
throw new ContractError('schema_invalid', 'asset schema is invalid');
throw err;
}
}
const store = deps.assetStore;
if (store) {
const fromStore = await findAssetInStore(ref, store);
// `not_found`, not `schema_invalid`: nothing was located, so no schema was
// ever read. `reuse` already reports this ref-resolution failure that way.
if (!fromStore)
throw new ContractError('not_found', 'asset not found');
return normalizeAsset(fromStore);
}
const assetsDir = contractAssetsDir(deps);
const readOnly = loadLocalAssetsReadOnly(assetsDir, ref);
const byAssetId = readOnly.filter((asset) => asset.asset_id === ref);
if (byAssetId.length > 1)
throw new ContractError('not_found', 'asset reference is ambiguous');
let found = byAssetId[0];
if (!found) {
const exactLogical = readOnly.filter((asset) => stringField(asset, 'id') === ref);
if (exactLogical.length > 1)
throw new ContractError('not_found', 'asset reference is ambiguous');
found = exactLogical[0];
}
if (!found) {
const fallback = prefixedLogicalRef(ref);
if (fallback) {
const matches = readOnly.filter((asset) => asset.type === fallback.kind && stringField(asset, 'id') === fallback.id);
if (matches.length > 1)
throw new ContractError('not_found', 'asset reference is ambiguous');
found = matches[0];
}
}
if (!found)
throw new ContractError('not_found', 'asset not found');
return normalizeAsset(found);
}
function prefixedLogicalRef(ref) {
const match = /^(gene|capsule):(.+)$/.exec(ref);
if (!match?.[2])
return null;
return { kind: match[1] === 'gene' ? 'Gene' : 'Capsule', id: match[2] };
}
async function findLogicalAsset(store, id, kind) {
if (store.findByLogicalId) {
const matches = await store.findByLogicalId(id, 2, kind);
if (matches.length > 1)
throw new ContractError('not_found', 'asset reference is ambiguous');
return matches[0] ?? null;
}
const scanLimit = 10_000;
const rows = await store.list(kind, scanLimit + 1);
if (rows.length > scanLimit)
throw new ContractError('not_found', 'asset lookup is truncated; use an exact asset_id');
const matches = rows.filter((asset) => stringField(asset, 'id') === id);
if (matches.length > 1)
throw new ContractError('not_found', 'asset reference is ambiguous');
return matches[0] ?? null;
}
async function findAssetInStore(ref, store) {
const byAssetId = await store.get(ref);
if (byAssetId)
return byAssetId;
const exact = await findLogicalAsset(store, ref);
if (exact)
return exact;
const fallback = prefixedLogicalRef(ref);
return fallback ? findLogicalAsset(store, fallback.id, fallback.kind) : null;
}
async function autoPairPublishRefs(refs, geneRef, deps) {
const store = deps.assetStore ?? new assetstore.LocalJsonlProvider(contractAssetsDir(deps));
if (!(store instanceof assetstore.LocalJsonlProvider)) {
throw new ContractError('bundle_required', '--auto-pair requires a local asset store; pass --capsule <id|path> explicitly');
}
const gene = await loadAssetRef(geneRef, { ...deps, assetStore: store });
if (gene.type !== 'Gene')
throw new ContractError('bundle_required', '--auto-pair requires a Gene reference');
const geneIds = new Set([gene.asset_id, stringField(gene, 'id')].filter((id) => Boolean(id)));
const trust = new assetstore.ProvenanceStore(store.baseDir).snapshot();
const candidates = store.listAll('Capsule').filter((candidate) => {
if (!geneIds.has(String(candidate['gene'] ?? '')))
return false;
const outcome = asRecord(candidate['outcome']);
if (outcome?.['status'] !== 'success')
return false;
if (trust.get(candidate.asset_id)?.trusted === false)
return false;
if (!wire.validateWireDeep(candidate).ok)
return false;
return wire.verifyAssetId(candidate);
});
if (candidates.length === 0) {
throw new ContractError('bundle_required', 'no eligible successful Capsule found; pass --capsule <id|path> explicitly');
}
if (candidates.length > 1) {
throw new ContractError('bundle_required', 'multiple eligible Capsules found; pass --capsule <id|path> explicitly');
}
return [...refs, candidates[0].asset_id];
}
function loadLocalAssetsReadOnly(baseDir, targetRef) {
return [
...loadLocalGenesReadOnly(baseDir, targetRef),
...readJsonLines(`${baseDir}/capsules.jsonl`, 'Capsule', targetRef),
...readJsonLines(`${baseDir}/events.jsonl`, 'EvolutionEvent', targetRef),
];
}
function loadLocalGenesReadOnly(baseDir, targetRef) {
const byId = new Map();
for (const gene of [
...readGenesEnvelopeReadOnly(`${baseDir}/genes.json`),
...readJsonLines(`${baseDir}/genes.jsonl`, 'Gene', targetRef),
]) {
const id = stringField(gene, 'id') ?? gene.asset_id;
if (id && !byId.has(id))
byId.set(id, gene);
}
return [...byId.values()];
}
function readGenesEnvelopeReadOnly(filePath) {
if (!existsSync(filePath))
return [];
try {
const parsed = JSON.parse(readFileSync(filePath, 'utf8'));
const envelopeGenes = isRecord(parsed) && Array.isArray(parsed['genes']) ? parsed['genes'] : [];
return envelopeGenes.filter(isRecord).map((gene) => normalizeAsset({ ...gene, type: 'Gene' }));
}
catch (err) {
if (err instanceof ContractError)
throw err;
if (err instanceof SyntaxError)
throw new ContractError('schema_invalid', 'asset schema is invalid');
throw err;
}
}
function readJsonLines(filePath, type, targetRef) {
if (!existsSync(filePath))
return [];
try {
return readFileSync(filePath, 'utf8')
.split('\n')
.map((line) => line.trim())
.filter(Boolean)
.map((line) => JSON.parse(line))
.filter((asset) => {
if (!isRecord(asset))
return false;
if (asset['type'] === type)
return true;
if (stringField(asset, 'asset_id') === targetRef || stringField(asset, 'id') === targetRef) {
throw new ContractError('schema_invalid', 'asset schema is invalid');
}
return false;
})
.map(normalizeAsset);
}
catch (err) {
if (err instanceof ContractError)
throw err;
if (err instanceof SyntaxError)
throw new ContractError('schema_invalid', 'asset schema is invalid');
throw err;
}
}
function normalizeAsset(value) {
if (!isRecord(value))
throw new ContractError('schema_invalid', 'asset is not an object');
const type = canonicalAssetType(value['type']);
if (!type)
throw new ContractError('schema_invalid', 'asset type must be Gene, Capsule, or EvolutionEvent');
return { ...value, type, asset_id: stringField(value, 'asset_id') ?? 'IGNORED' };
}
function checkBundle(bundle) {
const genes = bundle.filter((asset) => asset.type === 'Gene');
const capsules = bundle.filter((asset) => asset.type === 'Capsule');
const eventsFound = bundle.filter((asset) => asset.type === 'EvolutionEvent');
if (genes.length > 1 || capsules.length > 1 || eventsFound.length > 1) {
return { ok: false, message: 'publish supports one Gene + one Capsule + optional one EvolutionEvent bundle' };
}
if (genes.length === 0 || capsules.length === 0)
return { ok: false, message: BUNDLE_REQUIRED_MESSAGE };
const gene = genes[0];
const capsule = capsules[0];
const geneIds = new Set([gene.asset_id, stringField(gene, 'id')].filter((id) => Boolean(id)));
if (!geneIds.has(String(capsule['gene'] ?? '')))
return { ok: false, message: 'gene must publish with its capsule' };
return { ok: true };
}
function finalPayloadLeakCheck(bundle, env) {
const result = hub.fullLeakCheck(JSON.stringify(bundle), env);
const hardLeaks = result.leaks.filter((leak) => leak.type !== 'local_path');
return { blocked: hardLeaks.length > 0 };
}
async function assertNoLocalReuseIdConflict(asset, store) {
if (asset.type !== 'Gene' && asset.type !== 'Capsule')
return;
const id = stringField(asset, 'id');
if (!id)
return;
const rows = await store.list(asset.type, 10_000);
const local = rows.find((row) => stringField(row, 'id') === id);
if (!local)
return;
const incomingAssetId = wire.computeAssetId(asset);
const localAssetId = wire.computeAssetId(local);
if (incomingAssetId !== localAssetId)
throw new ContractError('internal_error', 'local asset id conflict');
}
function stripHubMetadata(asset) {
const out = {};
for (const [key, value] of Object.entries(asset))
if (!HUB_METADATA_KEYS.has(key))
out[key] = value;
return out;
}
function stripPublishMetadata(asset) {
return stripHubMetadata(asset);
}
function storeBaseDir(store, deps) {
return store instanceof assetstore.LocalJsonlProvider ? store.baseDir : contractAssetsDir(deps);
}
function contractAssetsDir(deps) {
return deps.assetsDir ?? events.assetsDir(deps.env ?? process.env);
}
function dryRunEnvelope(bundle, credits, blockDetail, repairAttempt) {
const suppressPayload = bundle.blockReasons.includes('leak_detected');
// Collect detailed messages for block reasons
const blockDetails = [];
if (blockDetail)
blockDetails.push(blockDetail);
for (const reason of bundle.blockReasons) {
const detailMessage = bundle.blockMessages?.[reason];
if (detailMessage && !blockDetails.includes(detailMessage)) {
blockDetails.push(detailMessage);
}
}
return {
ok: true,
contract: PUBLISH_CONTRACT,
mode: 'dry_run',
reversibility: REVERSIBILITY,
blocked: bundle.blockReasons.length > 0,
block_reasons: bundle.blockReasons,
...(blockDetails.length > 0 ? { block_details: blockDetails } : {}),
assets: bundle.assets,
...(suppressPayload ? {} : { payload: { assets: bundle.sanitized } }),
gates: bundle.gates,
...(credits ? { credits } : {}),
...repairEnvelopeField(repairAttempt),
};
}
function publishFailure(reason, message, opts) {
return {
ok: false,
contract: PUBLISH_CONTRACT,
...(opts.mode ? { mode: opts.mode } : {}),
...(opts.gates ? { gates: opts.gates } : {}),
...(opts.assets ? { assets: opts.assets } : {}),
...(opts.detail ? { detail: opts.detail } : {}),
...(opts.credits ? { credits: opts.credits } : {}),
...(opts.repair ? { repair: opts.repair } : {}),
reason,
retryable: opts.retryable,
message,
};
}
function reuseFailure(reason, message) {
return { ok: false, contract: REUSE_CONTRACT, reason, message };
}
function writeJson(out, value, code, deps) {
const chunk = `${JSON.stringify(sanitizeForContract(value, deps))}\n`;
if (out === process.stdout && machineJsonStdoutBypass)
machineJsonStdoutBypass(chunk);
else
out.write(chunk);
return code;
}
async function withMachineJsonConsole(enabled, deps, fn) {
if (!enabled)
return fn();
const original = {
log: console.log,
info: console.info,
warn: console.warn,
error: console.error,
debug: console.debug,
stdoutWrite: process.stdout.write,
stderrWrite: process.stderr.write,
};
const redirected = (...args) => {
try {
original.stderrWrite.call(process.stderr, `${sanitizeText(format(...args), deps)}\n`);
}
catch { /* ignore logging failures */ }
};
const stderrWrite = (chunk, encoding, callback) => {
const text = Buffer.isBuffer(chunk)
? chunk.toString(typeof encoding === 'string' ? encoding : 'utf8')
: String(chunk);
const clean = sanitizeText(text, deps);
const cb = typeof encoding === 'function' ? encoding : callback;
const ok = original.stderrWrite.call(process.stderr, clean);
cb?.();
return ok;
};
const stdoutWrite = (chunk, encoding, callback) => {
const text = Buffer.isBuffer(chunk)
? chunk.toString(typeof encoding === 'string' ? encoding : 'utf8')
: String(chunk);
const clean = sanitizeText(text, deps);
const cb = typeof encoding === 'function' ? encoding : callback;
const ok = original.stderrWrite.call(process.stderr, clean);
cb?.();
return ok;
};
const previousBypass = machineJsonStdoutBypass;
machineJsonStdoutBypass = (chunk) => original.stdoutWrite.call(process.stdout, chunk);
console.log = redirected;
console.info = redirected;
console.warn = redirected;
console.error = redirected;
console.debug = redirected;
process.stdout.write = stdoutWrite;
process.stderr.write = stderrWrite;
try {
return await fn();
}
finally {
console.log = original.log;
console.info = original.info;
console.warn = original.warn;
console.error = original.error;
console.debug = original.debug;
process.stdout.write = original.stdoutWrite;
process.stderr.write = original.stderrWrite;
machineJsonStdoutBypass = previousBypass;
}
}
async function composePublishedRecipe(transport, assets) {
if (!transport.composeRecipe)
return undefined;
try {
const composed = await transport.composeRecipe({
compose_recipe: true,
assets: [...assets],
});
return composed.ok && composed.recipeId ? composed.recipeId : undefined;
}
catch {
return undefined;
}
}
function createDefaultTransport(deps, opts = {}) {
const env = loadContractEnv(deps);
const hubMode = configuredHubMode(env);
if (hubMode === 'private') {
return createPrivateTransport(env, deps.resolveProxyClient ?? resolveDefaultPrivateProxy, opts.composeRecipe !== false);
}
const hubUrl = resolveHubUrl(env);
const evomapDir = resolveIdentityHome(env);
const explicitCredentials = resolveExplicitNodeCredentials(env);
const { nodeSecret } = explicitCredentials;
const sender = nodeSecret
? explicitCredentials.senderId ?? resolveAtpSenderId(env)
: resolveAtpSenderId(env);
const senderId = () => sender;
const connected = nodeSecret
? connectPublicHub({ hubUrl, authMode: 'legacy', evomapDir, nodeSecret, senderId })
: connectPublicHub({ hubUrl, authMode: 'oauth', evomapDir, senderId });
const http = new HubFetch({ baseUrl: hubUrl, auth: connected.auth, fetchFn: globalFetchLike, senderId });
const call = async (path, messageType, bundle) => {
try {
const body = await http.call('POST', path, gepEnvelope(messageType, { assets: [...bundle] }));
if (messageType === 'validate') {
const valid = hasExplicitValidatePass(body);
return { ok: valid, status: 200, body };
}
return { ok: true, status: 200, body };
}
catch (err) {
if (err instanceof AuthError)
return { ok: false, status: err.status };
if (isAuthLikeError(err))
return { ok: false, status: 401 };
if (err instanceof HubClientError)
return { ok: false, status: err.status, body: err.body };
return { ok: false, status: 0 };
}
};
return {
fetchAssetById: (assetId) => connected.hub.fetchAssetById(assetId),
validate: (bundle) => call('/a2a/validate', 'validate', bundle),
publish: (bundle) => call('/a2a/publish', 'publish', bundle),
...(opts.composeRecipe === false ? {} : {
composeRecipe: (payload) => hub.composeRecipeAfterAssetPublish(connected.hub, payload),
}),
};
}
function loadContractEnv(deps) {
const env = { ...(deps.env ?? process.env) };
const envFile = loadEnvFileFromEnv(env);
if (envFile.error)
throw new Error('failed to load EVOLVER_ENV_FILE');
return env;
}
function preserveInheritedSecrets(runtimeEnv, inheritedEnv) {
const env = { ...runtimeEnv };
let index = 0;
for (const [key, value] of Object.entries(inheritedEnv)) {
if (!value || value === runtimeEnv[key] || !/SECRET|TOKEN|API[_-]?KEY|PASSWORD|AUTH|CREDENTIAL/i.test(key))
continue;
env[`EVOLVER_INHERITED_SECRET_${index++}`] = value;
}
return env;
}
function configuredHubMode(env) {
const value = String(env['EVOMAP_HUB_MODE'] ?? 'public').trim().toLowerCase();
if (value === 'public' || value === 'private')
return value;
throw new Error('EVOMAP_HUB_MODE must be public or private');
}
function resolveDefaultPrivateProxy(env) {
return proxyClientFromEnv(env);
}
function createPrivateTransport(env, resolveProxy, composeRecipe = true) {
const proxy = resolveProxy(env);
if (!proxy)
throw new Error('private Hub proxy credentials are not configured');
const privateModeReady = assertPrivateProxyMode(proxy);
return {
validationCapabilityOptional: true,
fetchAssetById: async (assetId) => {
try {
await privateModeReady;
const body = await proxy.fetchAsset({ assetId, expectedHubMode: 'private' });
return privateProxyAssets(body).find((asset) => (asset.asset_id === assetId || stringField(asset, 'id') === assetId)) ?? null;
}
catch (err) {
if (err instanceof ContractError)
throw err;
const message = err instanceof Error ? err.message : String(err);
if (message === 'proxy_hub_mode_mismatch' || isAuthLikeError(err)) {
throw new ContractError('auth_required', 'private Hub proxy authentication required');
}
throw new ContractError('network_error', 'Hub unreachable');
}
},
validate: async (bundle) => {
try {
await privateModeReady;
const body = await proxy.validateAssetBundle({ assets: [...bundle], expected_hub_mode: 'private' });
return { ok: hasExplicitValidatePass(body), status: 200, body };
}
catch (err) {
if (err instanceof ContractError)
throw err;
return privateProxyFailure(err);
}
},
publish: async (bundle) => {
try {
await privateModeReady;
const body = normalizePrivatePublishBody(await proxy.submitAssetBundle({
assets: [...bundle],
expected_hub_mode: 'private',
...(composeRecipe ? {} : { compose_recipe: false }),
}));
return { ok: !privatePublishExplicitFailure(body) && normalizePublishStatus(body) !== undefined, status: 200, body };
}
catch (err) {
if (err instanceof ContractError)
throw err;
return privateProxyFailure(err);
}
},
};
}
async function assertPrivateProxyMode(proxy) {
const status = asRecord(await proxy.status()) ?? {};
if (status['hub_mode'] !== 'private') {
throw new ContractError('auth_required', 'private Hub proxy mode mismatch');
}
}
function isValidationCapabilityUnavailable(body) {
const root = asRecord(body) ?? {};
const payload = asRecord(root['payload']) ?? {};
return (root['reason'] ?? payload['reason']) === 'validate_not_configured';
}
function privateProxyAssets(body) {
const root = asRecord(body) ?? {};
const payload = asRecord(root['payload']) ?? {};
const rows = Array.isArray(root['assets']) ? root['assets'] : Array.isArray(payload['assets']) ? payload['assets'] : [];
return rows.filter((asset) => Boolean(asset && typeof asset === 'object' && !Array.isArray(asset)));
}
function normalizePrivatePublishBody(body) {
const root = asRecord(body);
if (!root)
return body;
const payload = asRecord(root['payload']);
if (payload) {
return { ...root, payload: normalizePrivatePublishLayer(payload) };
}
return normalizePrivatePublishLayer(root);
}
function normalizePrivatePublishLayer(body) {
const receiptId = stringField(body, 'receipt_id') ?? stringField(body, 'receiptId');
return {
...body,
...(stringField(body, 'status') === 'pending' ? { status: 'queued' } : {}),
...(receiptId ? { receipt_id: receiptId } : {}),
};
}
function privatePublishExplicitFailure(body) {
const root = asRecord(body) ?? {};
const payload = asRecord(root['payload']) ?? {};
return root['ok'] === false || root['stored'] === false || payload['ok'] === false || payload['stored'] === false;
}
function privateProxyFailure(err) {
return { ok: false, status: isAuthLikeError(err) ? 401 : 0 };
}
function isAuthLikeError(err) {
const message = err instanceof Error ? err.message : String(err);
return /oauth|login|credential|auth|401|403|node_secret/i.test(message);
}
function classifyError(err, command, env = {}) {
if (err instanceof ContractError)
return { reason: err.reason, message: err.safeMessage, retryable: err.reason === 'network_error' };
if (err instanceof AuthError)
return {
reason: 'auth_required',
message: command === 'publish' ? publishAuthRequiredMessage(env) : 'Hub authentication required',
retryable: false,
};
if (err instanceof HubUnreachableError)
return { reason: 'network_error', message: 'Hub unreachable', retryable: true };
if (err instanceof HubClientError) {
const reason = stableContractReasonFromBody(err.body) ?? (command === 'reuse' ? reuseReasonFromStatus(err.status) : publishReasonFromStatus(err.status));
return { reason, message: command === 'publish' ? publishReasonMessage(reason, env) : reuseReasonMessage(reason), retryable: publishRetryable(reason) };
}
const message = err instanceof Error ? err.message : String(err);
if (/oauth|login|credential|auth|401|403|node_secret/i.test(message))
return {
reason: 'auth_required',
message: command === 'publish' ? publishAuthRequiredMessage(env) : 'Hub authentication required',
retryable: false,
};
if (/network|fetch failed|ECONN|ENOTFOUND|ETIMEDOUT|hub 5\d\d/i.test(message))
return { reason: 'network_error', message: 'Hub unreachable', retryable: true };
return { reason: 'internal_error', message: `evolver ${command} failed`, retryable: false };
}
function reuseReasonFromStatus(status) {
if (status === 401 || status === 403)
return 'auth_required';
if (status === 404)
return 'not_found';
if (status === 429 || status >= 500 || status === 0)
return 'network_error';
return 'internal_error';
}
function publishReasonFromStatus(status) {
if (status === 401 || status === 403)
return 'auth_required';
if (status === 402)
return 'insufficient_credits';
if (status === 429 || status >= 500 || status === 0)
return 'network_error';
return 'quality_gate_failed';
}
function publishReasonFromResponse(status, body) {
return stableContractReasonFromBody(body) ?? publishReasonFromStatus(status);
}
function publishRetryable(reason) {
return reason === 'network_error';
}
function publishAuthRequiredMessage(env) {
const rawMode = env['EVOMAP_HUB_MODE']?.trim().toLowerCase();
if (rawMode && rawMode !== 'public' && rawMode !== 'private') {
return 'Hub authentication failed; verify EVOMAP_HUB_MODE and the configured credentials';
}
if (rawMode === 'private')
return PRIVATE_AUTH_REQUIRED_MESSAGE;
return resolveExplicitNodeCredentials(env).nodeSecret
? LEGACY_AUTH_REQUIRED_MESSAGE
: OAUTH_AUTH_REQUIRED_MESSAGE;
}
function publishReasonMessage(reason, env = {}) {
const map = {
missing_id: 'missing asset id',
cli_unavailable: 'evolver CLI unavailable',
auth_required: publishAuthRequiredMessage(env),
not_found: 'asset not found',
network_error: 'Hub unreachable',
unsupported: 'publish unsupported',
internal_error: 'evolver publish failed',
redaction_unavailable: 'redaction unavailable',
leak_detected: 'leak detected after redaction',
schema_invalid: 'asset schema is invalid',
bundle_required: BUNDLE_REQUIRED_MESSAGE,
quality_gate_failed: 'Hub quality gate failed',
gene_unproven: 'gene has no proven success yet — run it to a successful outcome before publishing',
insufficient_credits: 'insufficient credits',
unsafe_validation_command: 'validation command contains unsafe patterns (e.g., node -e, shell metacharacters) blocked by sandbox security policy',
};
return map[reason] ?? map.internal_error;
}
function reuseReasonMessage(reason) {
if (reason === 'not_found')
return 'asset not found';
if (reason === 'auth_required')
return 'Hub authentication required';
if (reason === 'network_error')
return 'Hub unreachable';
return 'evolver reuse failed';
}
function summarizePublishAssets(assets) {
return assets.map((asset) => {
const type = canonicalAssetType(asset.type);
return {
...(asset.asset_id ? { asset_id: asset.asset_id } : {}),
...(type ? { type } : {}),
};
});
}
function extractCredits(body) {
const payload = payloadRecord(body);
const credits = asRecord(payload['credits']) ?? asRecord(payload['credit_cost']) ?? asRecord(payload['economic']) ?? payload;
return creditsFromPayload(credits);
}
function creditsFromPayload(payload) {
const required = numberField(payload, 'required');
const available = numberField(payload, 'available');
const estimated = numberField(payload, 'estimated') ?? numberField(payload, 'estimate');
const charged = numberField(payload, 'charged');
const balanceKind = safeTokenField(stringField(payload, 'balance_kind') ?? stringField(payload, 'balanceKind'));
const out = {};
if (required !== undefined)
out['required'] = required;
if (available !== undefined)
out['available'] = available;
if (estimated !== undefined)
out['estimated'] = estimated;
if (charged !== undefined)
out['charged'] = charged;
if (balanceKind)
out['balance_kind'] = balanceKind;
return Object.keys(out).length > 0 ? out : undefined;
}
export function hasExplicitValidatePass(body) {
const payload = payloadRecord(body);
const hasExplicitPass = payload['valid'] === true || payload['ok'] === true;
const hasExplicitFail = payload['valid'] === false || payload['ok'] === false;
return hasExplicitPass && !hasExplicitFail;
}
function normalizePublishStatus(body) {
const payload = payloadRecord(body);
const status = stringField(payload, 'status');
if (status === 'candidate')
return 'queued';
if (status === 'queued' || status === 'accepted' || status === 'published')
return status;
if (status)
return undefined;
const decision = stringField(payload, 'decision');
if (decision === 'accept')
return 'accepted';
if (decision === 'accepted')
return 'accepted';
if ((decision === 'reject' || decision === 'rejected') && stringField(payload, 'reason') === 'already_published')
return 'published';
return undefined;
}
function payloadRecord(body) {
const root = asRecord(body) ?? {};
return asRecord(root['payload']) ?? root;
}
// The Hub answers a rejected publish with the SPECIFIC rule that failed
// ("gene_validation_required: ..."), which our stable ContractReason enum
// necessarily flattens to `quality_gate_failed`. Keeping only the flattened code
// leaves every caller — the desktop wizard above all — with a red gate and no
// way to act on it, so the detail rides along in a separate additive field.
// Capped and control-character-stripped: it is Hub-authored text that ends up in
// a machine-readable envelope on stdout.
const HUB_DETAIL_MAX_CHARS = 300;
function stripControlChars(value) {
let out = '';
for (const ch of value) {
const code = ch.codePointAt(0) ?? 0;
out += code < 0x20 || code === 0x7f ? ' ' : ch;
}
return out;
}
function hubDetailFromBody(body) {
const root = asRecord(body) ?? {};
const payload = payloadRecord(body);
// Take the first candidate that ISN'T already a stable contract token: those
// are what `reason` carries, so emitting one here would repeat the flattened
// code this field exists to supplement. The Hub is free to put the token in
// `reason` and the rule text in `error` (or the reverse), so order alone
// cannot pick the informative one.
const raw = [
stringField(root, 'error'),
stringField(root, 'reason'),
stringField(payload, 'error'),
stringField(payload, 'reason'),
].find((candidate) => candidate && !isStableContractReason(candidate.trim()));
if (!raw)
return undefined;
const detail = stripControlChars(raw).replace(/\s+/g, ' ').trim();
if (!detail)
return undefined;
return detail.length > HUB_DETAIL_MAX_CHARS ? `${detail.slice(0, HUB_DETAIL_MAX_CHARS)}` : detail;
}
function stableContractReasonFromBody(body) {
const payload = payloadRecord(body);
const reason = stringField(payload, 'reason') ?? stringField(payload, 'error');
return isStableContractReason(reason) ? reason : undefined;
}
function isStableContractReason(value) {
return Boolean(value && STABLE_CONTRACT_REASONS.has(value));
}
function sanitizeForContract(value, deps) {
return redactKnownSecrets(hub.redactDeep(value), deps);
}
function sanitizeText(value, deps) {
return String(redactKnownSecrets(hub.redactString(value), deps));
}
function redactKnownSecrets(value, deps, known = knownLocalSecrets(deps)) {
if (typeof value === 'string')
return redactKnownSecretsInString(value, known);
if (!value || typeof value !== 'object')
return value;
if (Array.isArray(value))
return value.map((item) => redactKnownSecrets(item, deps, known));
const out = {};
for (const [key, item] of Object.entries(value))
out[key] = redactKnownSecrets(item, deps, known);
return out;
}
function redactKnownSecretsInString(value, known) {
let result = value;
for (const secret of known)
result = result.split(secret).join('[REDACTED]');
return result;
}
function knownLocalSecrets(deps) {
const env = deps.env ?? process.env;
const secrets = new Set();
for (const [key, value] of Object.entries(env)) {
if (typeof value === 'string' && value.length >= 8 && /SECRET|TOKEN|API[_-]?KEY|PASSWORD|AUTH|CREDENTIAL/i.test(key)) {
secrets.add(value);
}
}
return [...secrets].sort((a, b) => b.length - a.length);
}
function canonicalAssetType(value) {
if (value === 'Gene' || value === 'gene')
return 'Gene';
if (value === 'Capsule' || value === 'capsule')
return 'Capsule';
if (value === 'EvolutionEvent' || value === 'event' || value === 'Evolutionevent')
return 'EvolutionEvent';
return undefined;
}
function looksLikeFile(value) {
try {
return existsSync(value) && statSync(value).isFile();
}
catch {
return false;
}
}
function isRecord(value) {
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
}
function asRecord(value) {
return isRecord(value) ? value : undefined;
}
function stringField(value, key) {
const record = asRecord(value);
const raw = record?.[key];
return typeof raw === 'string' && raw.trim() ? raw.trim() : undefined;
}
function numberField(value, key) {
const raw = asRecord(value)?.[key];
const n = typeof raw === 'number' ? raw : (typeof raw === 'string' && raw.trim() ? Number(raw) : NaN);
return Number.isFinite(n) ? n : undefined;
}
function safeTokenField(value) {
if (!value)
return undefined;
return /^[A-Za-z0-9_.:-]{1,64}$/.test(value) ? value : undefined;
}
export function _inspectCliContractsForTest(value) {
return inspect(value, { depth: 4, colors: false });
}