Files
2026-09-16 19:33:47 +08:00

153 lines
9.1 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.
// Secure-by-construction autonomous execution: compose ALL the exec-bridge safety controls into one factory
// so an unattended caller (a resident daemon) can't accidentally omit a control. This is the productized
// counterpart to the scratch daemon — the daemon becomes a thin caller of makeSafeExecute().
import { isExecutionEligible } from '../reference/guard.js';
import { CLAUDE_SAFE_AUTONOMOUS_TOOLS, hasBoundedClaudeFileAccess, makeClaudeExecBridge, validateAgentSessionResume } from './claudeBridge.js';
import { assessSourceEligibility } from '../assetstore/sourceQualification.js';
import { sha256Hash } from '../schema/common.js';
const asStrings = (v) => (Array.isArray(v) ? v.filter((x) => typeof x === 'string') : []);
/**
* Resolve a gene's strategy from the store and whether it is safe to EMBED into an autonomous agent's prompt —
* the exec-side link from #30 (provenance ledger) and the review-state gate to #45 (requireTrustedGene gate). A
* gene is embeddable only when BOTH axes pass: trusted ORIGIN (no provenance record → local/trusted; a hub one
* is untrusted until promoted) AND review-APPROVED content (no review record → eligible; an auto-distilled draft
* is quarantined until a human approves). Both default-open, so cycle-self-produced/local genes are unaffected;
* only hub-ingested (untrusted) and auto-distilled (unreviewed) drafts are withheld. Looks up by id or asset_id.
*/
export function makeTrustedGeneResolver(store, provenance, review, includeProbation = false, benchmark) {
return async (geneId) => {
const genes = await store.list('Gene', 1000);
// 只识别完整digest;长度检查也防止正则$把尾随换行当作结束位置。
const canonicalOnly = benchmark !== undefined && geneId.length === 71 && sha256Hash.safeParse(geneId).success;
const g = genes.find((x) => {
// benchmark逻辑ID先过滤来源资格;canonical引用不能回退到其他资产的logical别名。
const matches = String(x.asset_id) === geneId || (!canonicalOnly && String(x['id']) === geneId);
return matches && (!benchmark || assessSourceEligibility(x, provenance?.get(x.asset_id), benchmark).allowed);
});
if (!g || !isExecutionEligible(g, store.referenceScope))
return null;
const summary = g['summary'];
const trustedOrigin = provenance ? provenance.isTrusted(String(g.asset_id)) : true;
const reviewApproved = review ? review.isApproved(String(g.asset_id)) : true;
// Probation (#306): when enabled, a QUARANTINED (auto-distilled, not yet approved — but not rejected) draft IS
// embeddable, so its strategy actually drives the trial. That is what makes the trial's outcome real evidence
// for auto-promote (without embedding, the gene would be selected but run as innovate — hollow evidence). The
// strategy is unreviewed, so its containment rests on the proven exec gates: sanitizeInjection neutralizes
// injected directives in the prompt, and the always-on hard gates + worktree isolation contain the agent's
// ACTIONS regardless of the prompt (#309). A REJECTED draft and an untrusted-origin (hub) gene stay withheld.
const probationOk = includeProbation && review?.get(String(g.asset_id))?.state === 'quarantined';
const info = {
strategy: asStrings(g['strategy']),
preconditions: asStrings(g['preconditions']),
...(typeof summary === 'string' ? { summary } : {}),
trusted: trustedOrigin && (reviewApproved || probationOk), // trusted ORIGIN + (approved OR on probation)
};
return info;
};
}
// Keep Claude's project path checks enabled and expose only file/search tools.
// Bash, web, task, and MCP tools remain unavailable.
const CLAUDE_DEFAULT_AGENT_OPTIONS = {
permissionMode: 'acceptEdits',
tools: CLAUDE_SAFE_AUTONOMOUS_TOOLS,
};
// Codex workspace-write prevents host writes but does not contain host reads. It therefore remains explicit.
const CODEX_DEFAULT_AGENT_OPTIONS = {};
// Cursor has no verified host filesystem/network sandbox. Worktree isolation cannot safely authorize `--trust`.
const CURSOR_DEFAULT_AGENT_OPTIONS = {};
// Gemini's verified safe default is `--approval-mode auto_edit`; shell remains gated and --yolo is refused.
const GEMINI_DEFAULT_AGENT_OPTIONS = {};
// The llm runner has no host permission prompts to bypass and no vendor tool names to allow:
// its tools are its own, and they are bounded by construction.
const LLM_DEFAULT_AGENT_OPTIONS = {};
/** Per-runner safe default agent options. Host permission bypass is never enabled by default. */
function defaultAgentOptions(runner) {
if (runner === 'llm')
return LLM_DEFAULT_AGENT_OPTIONS;
if (runner === 'codex')
return CODEX_DEFAULT_AGENT_OPTIONS;
if (runner === 'cursor')
return CURSOR_DEFAULT_AGENT_OPTIONS;
if (runner === 'gemini')
return GEMINI_DEFAULT_AGENT_OPTIONS;
return CLAUDE_DEFAULT_AGENT_OPTIONS;
}
export class UnsupportedCursorAutonomousIsolationError extends Error {
constructor() {
super('Autonomous Cursor cannot use worktree isolation because --trust grants host filesystem and network access');
this.name = 'UnsupportedCursorAutonomousIsolationError';
}
}
export class UnsupportedAutonomousHostAccessError extends Error {
constructor() {
super('Autonomous Claude requires project-scoped acceptEdits with bounded file/search tools; host access bypass or unsupported tools are forbidden because a worktree is not a security boundary');
this.name = 'UnsupportedAutonomousHostAccessError';
}
}
export class UnsupportedAutonomousClaudeRunnerError extends Error {
constructor() {
super('Autonomous claude is disabled because its headless edit mode has no verified host filesystem sandbox; inject an externally sandboxed agent or explicitly select a runner after reviewing its host-access boundary');
this.name = 'UnsupportedAutonomousClaudeRunnerError';
}
}
export class UnsupportedAutonomousCodexRunnerError extends Error {
constructor() {
super('Autonomous codex is disabled because workspace-write does not restrict host filesystem reads; inject an externally sandboxed agent');
this.name = 'UnsupportedAutonomousCodexRunnerError';
}
}
export function resolveAutonomousAgentOptions(runner, isolation, overrides) {
const options = {
...defaultAgentOptions(runner),
...(isolation !== 'none' ? { workspaceTrust: 'isolated-worktree' } : {}),
...(overrides ?? {}),
};
if (runner === 'cursor' && isolation !== 'none') {
throw new UnsupportedCursorAutonomousIsolationError();
}
const effectiveRunner = runner ?? 'claude';
const unsafeClaudeOptions = effectiveRunner === 'claude' && !hasBoundedClaudeFileAccess(options);
if (unsafeClaudeOptions) {
throw new UnsupportedAutonomousHostAccessError();
}
return options;
}
/**
* Build the fully-hardened `execute` for an autonomous run against `repo`. Composes every exec-bridge control
* with secure defaults so they can't be forgotten piecemeal: deny-by-default allowedRoots (#41) + worktree
* isolation (#43) + env scrub (#42) + non-bypassing agent permissions (#38/#40) + trusted-gene gate fed by
* provenance (#45/#30). Pass the result as runEvolutionCycle's `execute`.
*/
export function makeSafeExecute(repo, store, safety, opts = {}) {
const runner = safety.runner ?? (opts.agent ? undefined : 'claude');
if (safety.resume && runner)
validateAgentSessionResume(safety.resume, runner);
if (!opts.agent && runner === 'claude') {
throw new UnsupportedAutonomousClaudeRunnerError();
}
if (!opts.agent && runner === 'codex') {
throw new UnsupportedAutonomousCodexRunnerError();
}
return makeClaudeExecBridge({
cwd: repo,
enabled: true,
...(opts.agent ? { agent: opts.agent } : {}),
...(opts.git ? { git: opts.git } : {}),
...(opts.traceRecorder ? { traceRecorder: opts.traceRecorder } : {}),
...(opts.executionObserver ? { executionObserver: opts.executionObserver } : {}),
...(opts.executionLimits ? { executionLimits: opts.executionLimits } : {}),
allowedRoots: safety.allowedRoots,
...(runner ? { runner } : {}),
...(safety.resume ? { resume: safety.resume } : {}),
...(safety.isolation === 'none' ? {} : { isolation: 'worktree' }),
scrubEnv: safety.scrubEnv ?? true,
requireTrustedGene: safety.requireTrustedGene ?? true,
agentOptions: resolveAutonomousAgentOptions(runner, safety.isolation, safety.agentOptions),
...(safety.timeoutMs !== undefined ? { timeoutMs: safety.timeoutMs } : {}),
...(safety.signal ? { signal: safety.signal } : {}),
resolveGene: makeTrustedGeneResolver(store, opts.provenance, opts.review, opts.includeProbation ?? false, opts.benchmark),
...(opts.validate ? { validate: opts.validate } : {}),
...(opts.validationCmds ? { validationCmds: opts.validationCmds } : {}),
...(opts.personality ? { personality: opts.personality } : {}),
});
}