mirror of
https://gh-proxy.org/https://github.com/klinsmaya/portal-template-default.git
synced 2026-11-09 01:27:53 +08:00
89 lines
4.3 KiB
JavaScript
89 lines
4.3 KiB
JavaScript
import { agentConfig } from './agent-configuration.mjs';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { credentialNames } from './agent-adapter.mjs';
|
|
import { scrubHistoryFile, scrubSecrets } from './history-redaction.mjs';
|
|
// Capture only factory-visible inputs. CLI-internal prompts/subagents are not
|
|
// reconstructed, and hashing a Skill is not evidence that the Agent read it.
|
|
import { createHash, randomUUID } from 'node:crypto';
|
|
import { lstatSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
import path from 'node:path';
|
|
import { buildRedactor } from './agent-harness.mjs';
|
|
|
|
const sha256 = (value) => createHash('sha256').update(value).digest('hex');
|
|
|
|
export function beginInvocation({ log, prompt, workspace, engine, phase, secrets, env = process.env, contextFiles = [] }) {
|
|
const knownRedact = buildRedactor(secrets);
|
|
const redact = value => scrubSecrets(knownRedact(value));
|
|
const context = [];
|
|
function collect(relative) {
|
|
const file = path.join(workspace, relative);
|
|
let stat;
|
|
try { stat = lstatSync(file); }
|
|
catch (error) { if (error.code === 'ENOENT') return; throw error; }
|
|
if (stat.isSymbolicLink()) return;
|
|
if (stat.isDirectory()) {
|
|
for (const name of readdirSync(file).sort()) collect(`${relative}/${name}`);
|
|
} else if (stat.isFile()) {
|
|
context.push({ path: relative, sha256: sha256(readFileSync(file)) });
|
|
}
|
|
}
|
|
for (const file of ['AGENTS.md', 'pnpm-lock.yaml', 'factory-template.json', '.agents/skills', ...contextFiles]) collect(file);
|
|
mkdirSync(path.dirname(log), { recursive: true });
|
|
const text = redact(readFileSync(prompt, 'utf8'));
|
|
writeFileSync(`${log}.prompt.md`, text, { mode: 0o600 });
|
|
const record = {
|
|
version: 1,
|
|
id: `${env.GITHUB_RUN_ID || 'local'}:${env.GITHUB_RUN_ATTEMPT || '1'}:${phase}:${randomUUID()}`,
|
|
engine, phase, configuration: agentConfig({ ...env, CODE_AGENT_ENGINE: engine }), startedAt: new Date().toISOString(), status: 'preparing', invoked: false,
|
|
controlSha: env.FACTORY_CONTROL_SHA || null,
|
|
promptSha256: sha256(text), context,
|
|
boundary: 'Factory prompt, invocation settings and available Skill hashes only; not CLI-internal prompts or proof of Skill usage.',
|
|
};
|
|
function save() {
|
|
// JSON-encode individual strings after redaction, preserving valid JSON even
|
|
// when a credential contains quotes or backslashes.
|
|
writeFileSync(`${log}.invocation.json`, `${JSON.stringify(record, (_key, value) =>
|
|
typeof value === 'string' ? redact(value) : value, 2)}\n`, { mode: 0o600 });
|
|
}
|
|
save();
|
|
return {
|
|
start({ command, args, model, actualVersion, configuredVersion }) {
|
|
Object.assign(record, { command, args, model, actualVersion, configuredVersion,
|
|
invoked: true, status: 'running' });
|
|
save();
|
|
},
|
|
finish(error) {
|
|
Object.assign(record, { status: error ? 'failed' : 'finished',
|
|
endedAt: new Date().toISOString(), error: error?.message });
|
|
save();
|
|
},
|
|
};
|
|
}
|
|
|
|
// Runs in the existing credential-bearing reply job, never the trusted publisher.
|
|
// Re-scrub on the always() path, including interrupted calls and partial replies.
|
|
export function stageReply(directory, metadata, env = process.env) {
|
|
const target = path.join(directory, 'comment-artifacts');
|
|
mkdirSync(target, { recursive: true });
|
|
const redact = buildRedactor([...credentialNames.map(name => env[name]),
|
|
env.FACTORY_ADMIN_PASSWORD, env.FACTORY_TEST_PASSWORD]);
|
|
for (const [source, name] of [[path.join(directory, 'comment-reply.md'), 'comment-reply.md'],
|
|
[metadata, 'task-metadata.json']]) {
|
|
try {
|
|
if (!lstatSync(source).isFile()) continue;
|
|
writeFileSync(path.join(target, name), scrubHistoryFile(readFileSync(source, 'utf8'), name, redact), { mode: 0o600 });
|
|
} catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
}
|
|
for (const name of readdirSync(target)) {
|
|
if (!/^comment-agent\.jsonl(?:\.(?:result|invocation)\.json|\.prompt\.md)?$/.test(name)) continue;
|
|
const file = path.join(target, name);
|
|
if (!lstatSync(file).isFile()) continue;
|
|
writeFileSync(file, scrubHistoryFile(readFileSync(file, 'utf8'), name, redact), { mode: 0o600 });
|
|
}
|
|
}
|
|
|
|
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
if (process.argv[2] !== 'stage-reply') throw new Error('Expected stage-reply');
|
|
stageReply(process.argv[3], process.argv[4]);
|
|
}
|