guanghu-ice-heart/server-tools/dark-core/local-file-host.mjs

50 lines
2.6 KiB
JavaScript
Raw Normal View History

import fs from 'node:fs';
import path from 'node:path';
import { createHash } from 'node:crypto';
import { digest } from './task-controller.mjs';
const hash = bytes => createHash('sha256').update(bytes).digest('hex');
// A deliberately bounded local adapter, not a shell, remote executor, or approval issuer.
// Call only after the real host has authorized approvedPlan. No input file authenticates itself.
export function localFileHost({ root, approvedPlan, approvalReceipt, verifyHuman }) {
if (!path.isAbsolute(root) || !fs.statSync(root).isDirectory() || fs.realpathSync(root) !== root ||
typeof approvalReceipt !== 'string' || !approvalReceipt.trim() || typeof verifyHuman !== 'function') {
throw Error('TRUSTED_HOST_CONFIGURATION_REQUIRED');
}
const approved = structuredClone(approvedPlan);
const expected = digest(approved);
const registeredGrants = new WeakSet();
for (const step of approved.steps) {
if (step.capability !== 'CREATE_TEXT_FILE' || typeof step.args.text !== 'string' ||
typeof step.args.path !== 'string' || path.dirname(step.args.path) !== root ||
path.basename(step.args.path) === '.' || path.basename(step.args.path) === '..' ||
step.args.path !== path.resolve(step.args.path)) throw Error('LOCAL_CAPABILITY_OUT_OF_SCOPE');
}
return {
verifyHuman,
async authorize({ plan, step, planHash, signal }) {
signal.throwIfAborted();
if (digest(plan) !== expected || planHash !== expected ||
!approved.steps.some(s => digest(s) === digest(step))) throw Error('PLAN_NOT_AUTHORIZED');
const grant = Object.freeze({ planHash, stepId: step.id, receipt: approvalReceipt });
registeredGrants.add(grant);
return grant;
},
async execute({ step, grant, signal }) {
signal.throwIfAborted();
if (!registeredGrants.has(grant)) throw Error('UNTRUSTED_GRANT');
registeredGrants.delete(grant);
// Recheck after authorization; wx never overwrites an existing file or symlink.
if (fs.realpathSync(root) !== root) throw Error('ROOT_CHANGED');
fs.writeFileSync(step.args.path, step.args.text, { flag: 'wx', mode: 0o600 });
return { kind: 'FILE_CREATED', path: step.args.path, sha256: hash(fs.readFileSync(step.args.path)) };
},
async verify({ step, result }) {
const stat = fs.lstatSync(step.args.path);
return stat.isFile() && !stat.isSymbolicLink() && result?.kind === 'FILE_CREATED' &&
result.path === step.args.path && result.sha256 === hash(Buffer.from(step.args.text)) &&
hash(fs.readFileSync(step.args.path)) === result.sha256;
},
};
}