425 lines
12 KiB
JavaScript
Executable file
425 lines
12 KiB
JavaScript
Executable file
#!/usr/bin/env node
|
|
import crypto from "node:crypto";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { execFileSync } from "node:child_process";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
|
|
function required(value, name) {
|
|
const text = String(value || "").trim();
|
|
if (!text) throw new Error(`${name}_required`);
|
|
return text;
|
|
}
|
|
|
|
function git(args, options = {}) {
|
|
return execFileSync("/usr/bin/git", args, {
|
|
encoding: "utf8",
|
|
maxBuffer: 8 * 1024 * 1024,
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
...options,
|
|
}).trim();
|
|
}
|
|
|
|
function gitBare(repository, ...args) {
|
|
return git([`--git-dir=${repository}`, ...args]);
|
|
}
|
|
|
|
function readJsonFromGit(repository, revision, file) {
|
|
try {
|
|
return JSON.parse(gitBare(repository, "show", `${revision}:${file}`));
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function sourceRange(repository, currentSha, cursorSha, windowSize) {
|
|
let fromSha = cursorSha;
|
|
if (
|
|
!/^[0-9a-f]{40}$/.test(String(fromSha || "")) ||
|
|
(() => {
|
|
try {
|
|
gitBare(repository, "merge-base", "--is-ancestor", fromSha, currentSha);
|
|
return false;
|
|
} catch {
|
|
return true;
|
|
}
|
|
})()
|
|
) {
|
|
const recent = gitBare(
|
|
repository,
|
|
"rev-list",
|
|
`--max-count=${windowSize + 1}`,
|
|
currentSha,
|
|
)
|
|
.split("\n")
|
|
.filter(Boolean);
|
|
fromSha = recent.at(-1) || currentSha;
|
|
}
|
|
return { fromSha, toSha: currentSha };
|
|
}
|
|
|
|
function changedContext(repository, fromSha, toSha, limits = {}) {
|
|
const changedCommits = gitBare(
|
|
repository,
|
|
"log",
|
|
"--reverse",
|
|
"--format=%H%x09%s",
|
|
`${fromSha}..${toSha}`,
|
|
)
|
|
.split("\n")
|
|
.filter(Boolean)
|
|
.slice(0, limits.commits || 40);
|
|
const changedFiles = gitBare(
|
|
repository,
|
|
"diff",
|
|
"--name-only",
|
|
fromSha,
|
|
toSha,
|
|
)
|
|
.split("\n")
|
|
.filter(Boolean)
|
|
.slice(0, limits.files || 200);
|
|
|
|
const allowed = /\.(?:json|hdlp|md|mjs|js|service|timer)$/iu;
|
|
const excerptParts = [];
|
|
let used = 0;
|
|
const maxBytes = limits.excerptBytes || 80_000;
|
|
for (const file of changedFiles) {
|
|
if (!allowed.test(file) || used >= maxBytes) continue;
|
|
try {
|
|
const body = gitBare(repository, "show", `${toSha}:${file}`);
|
|
const remaining = maxBytes - used;
|
|
const excerpt = body.slice(0, Math.min(remaining, 8_000));
|
|
excerptParts.push(`FILE ${file}\n${excerpt}`);
|
|
used += Buffer.byteLength(excerpt, "utf8");
|
|
} catch {
|
|
// A deleted, renamed, or non-text file remains visible in changed_files.
|
|
}
|
|
}
|
|
return {
|
|
changedCommits,
|
|
changedFiles,
|
|
changeExcerpts: excerptParts.join("\n\n"),
|
|
};
|
|
}
|
|
|
|
function priorPersonaContext(repository) {
|
|
const current = readJsonFromGit(
|
|
repository,
|
|
"refs/heads/main",
|
|
"persona-system/fifth-domain/CURRENT.json",
|
|
);
|
|
if (!current) return "";
|
|
let observation = null;
|
|
if (typeof current.artifact === "string") {
|
|
observation = readJsonFromGit(
|
|
repository,
|
|
"refs/heads/main",
|
|
current.artifact,
|
|
);
|
|
}
|
|
return JSON.stringify({ current, last_observation: observation }).slice(
|
|
0,
|
|
48_000,
|
|
);
|
|
}
|
|
|
|
function verifyMemberResponse(response, member, event) {
|
|
if (
|
|
response?.ok !== true ||
|
|
response?.persona_id !== member.persona_id ||
|
|
response?.identity_fingerprint !== member.identity_fingerprint ||
|
|
response?.signature_algorithm !== "Ed25519" ||
|
|
response?.payload?.team_controller_id !== "ICE-P-ZY001" ||
|
|
response?.payload?.source?.to_sha !== event.source.to_sha ||
|
|
response?.payload?.caller_nonce !== event.caller_nonce
|
|
) {
|
|
throw new Error(`${member.slug}_response_identity_mismatch`);
|
|
}
|
|
const valid = crypto.verify(
|
|
null,
|
|
Buffer.from(JSON.stringify(response.payload)),
|
|
response.public_key,
|
|
Buffer.from(response.signature, "base64"),
|
|
);
|
|
if (!valid) throw new Error(`${member.slug}_signature_invalid`);
|
|
return response;
|
|
}
|
|
|
|
async function requestObservation(member, event, fetchImpl = globalThis.fetch) {
|
|
const controllerToken = required(
|
|
member.controller_token,
|
|
`${member.slug}_controller_token`,
|
|
);
|
|
const response = await fetchImpl(member.endpoint, {
|
|
method: "POST",
|
|
headers: {
|
|
"content-type": "application/json",
|
|
"x-guanghu-controller-token": controllerToken,
|
|
},
|
|
body: JSON.stringify(event),
|
|
signal: AbortSignal.timeout(180_000),
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error(`${member.slug}_observation_http_${response.status}`);
|
|
}
|
|
return verifyMemberResponse(await response.json(), member, event);
|
|
}
|
|
|
|
function commitObservation({
|
|
member,
|
|
response,
|
|
event,
|
|
targetRepository,
|
|
runtimeRoot,
|
|
now,
|
|
}) {
|
|
const temporaryRoot = fs.mkdtempSync(
|
|
path.join(runtimeRoot, "tmp", `${member.slug}-`),
|
|
);
|
|
const checkout = path.join(temporaryRoot, "repository");
|
|
try {
|
|
git(["clone", "--no-local", targetRepository, checkout]);
|
|
git(["config", "user.name", `${member.name}人格系统`], { cwd: checkout });
|
|
git(["config", "user.email", member.commit_email], { cwd: checkout });
|
|
|
|
const relativeRoot = "persona-system/fifth-domain";
|
|
const artifact =
|
|
`${relativeRoot}/observations/` +
|
|
`${now.slice(0, 10)}-${event.source.to_sha.slice(0, 12)}.json`;
|
|
const artifactPath = path.join(checkout, artifact);
|
|
fs.mkdirSync(path.dirname(artifactPath), { recursive: true });
|
|
fs.writeFileSync(
|
|
artifactPath,
|
|
`${JSON.stringify(
|
|
{
|
|
schema: "guanghu.persona-fifth-domain-update-record/v1",
|
|
persona_id: member.persona_id,
|
|
controller: "ICE-P-ZY001",
|
|
observed_at: now,
|
|
source: event.source,
|
|
changed_commits: event.changed_commits,
|
|
changed_files: event.changed_files,
|
|
observation: response.payload.observation,
|
|
proof: {
|
|
identity_fingerprint: response.identity_fingerprint,
|
|
signature_algorithm: response.signature_algorithm,
|
|
signature: response.signature,
|
|
},
|
|
},
|
|
null,
|
|
2,
|
|
)}\n`,
|
|
{ mode: 0o600 },
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(checkout, relativeRoot, "CURRENT.json"),
|
|
`${JSON.stringify(
|
|
{
|
|
schema: "guanghu.persona-fifth-domain-current/v1",
|
|
persona_id: member.persona_id,
|
|
controller: "ICE-P-ZY001",
|
|
source_repository: "REPO-012",
|
|
source_branch: "main",
|
|
last_source_sha: event.source.to_sha,
|
|
artifact,
|
|
updated_at: now,
|
|
update_rule:
|
|
"Daily wake reads Fifth Domain deltas; repository commits occur only when REPO-012 main advances.",
|
|
},
|
|
null,
|
|
2,
|
|
)}\n`,
|
|
{ mode: 0o600 },
|
|
);
|
|
git(["add", relativeRoot], { cwd: checkout });
|
|
git(
|
|
[
|
|
"commit",
|
|
"-m",
|
|
`第五域巡游:${event.source.to_sha.slice(0, 12)} · ${member.name}`,
|
|
],
|
|
{ cwd: checkout },
|
|
);
|
|
git(["push", "origin", "HEAD:refs/heads/main"], { cwd: checkout });
|
|
return {
|
|
repository_sha: git(["rev-parse", "HEAD"], { cwd: checkout }),
|
|
artifact,
|
|
};
|
|
} finally {
|
|
fs.rmSync(temporaryRoot, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
function loadConfig(file) {
|
|
const config = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
required(config.source_repository, "source_repository");
|
|
required(config.runtime_root, "runtime_root");
|
|
if (!Array.isArray(config.members) || config.members.length !== 3) {
|
|
throw new Error("exactly_three_members_required");
|
|
}
|
|
return config;
|
|
}
|
|
|
|
async function runDaily(config, options = {}) {
|
|
const fetchImpl = options.fetchImpl || globalThis.fetch;
|
|
const now = options.now || new Date().toISOString();
|
|
fs.mkdirSync(path.join(config.runtime_root, "tmp"), {
|
|
recursive: true,
|
|
mode: 0o700,
|
|
});
|
|
fs.mkdirSync(path.join(config.runtime_root, "receipts"), {
|
|
recursive: true,
|
|
mode: 0o700,
|
|
});
|
|
const sourceSha = gitBare(
|
|
config.source_repository,
|
|
"rev-parse",
|
|
"refs/heads/main",
|
|
);
|
|
const results = await Promise.all(config.members.map(async (member) => {
|
|
try {
|
|
const targetRepository = required(
|
|
member.target_repository,
|
|
`${member.slug}_target_repository`,
|
|
);
|
|
const current = readJsonFromGit(
|
|
targetRepository,
|
|
"refs/heads/main",
|
|
"persona-system/fifth-domain/CURRENT.json",
|
|
);
|
|
if (current?.last_source_sha === sourceSha) {
|
|
return {
|
|
persona_id: member.persona_id,
|
|
status: "NO_NEW_FIFTH_DOMAIN_COMMIT",
|
|
source_sha: sourceSha,
|
|
};
|
|
}
|
|
const range = sourceRange(
|
|
config.source_repository,
|
|
sourceSha,
|
|
current?.last_source_sha,
|
|
config.bootstrap_commit_window || 20,
|
|
);
|
|
const delta = changedContext(
|
|
config.source_repository,
|
|
range.fromSha,
|
|
range.toSha,
|
|
config.limits,
|
|
);
|
|
const event = {
|
|
schema: "guanghu.fifth-domain-persona-observation-event/v1",
|
|
team_controller_id: "ICE-P-ZY001",
|
|
caller_nonce: crypto.randomBytes(18).toString("hex"),
|
|
source: {
|
|
repository_id: "REPO-012",
|
|
branch: "main",
|
|
from_sha: range.fromSha,
|
|
to_sha: range.toSha,
|
|
},
|
|
changed_commits: delta.changedCommits,
|
|
changed_files: delta.changedFiles,
|
|
change_excerpts: delta.changeExcerpts,
|
|
prior_persona_context: priorPersonaContext(targetRepository),
|
|
scoped_duties: member.scoped_duties,
|
|
};
|
|
const response = await requestObservation(member, event, fetchImpl);
|
|
const committed = commitObservation({
|
|
member,
|
|
response,
|
|
event,
|
|
targetRepository,
|
|
runtimeRoot: config.runtime_root,
|
|
now,
|
|
});
|
|
return {
|
|
persona_id: member.persona_id,
|
|
status: "FIFTH_DOMAIN_UPDATE_COMMITTED",
|
|
source_sha: sourceSha,
|
|
...committed,
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
persona_id: member.persona_id,
|
|
status: "FAILED",
|
|
error: String(error.message || error).slice(0, 300),
|
|
};
|
|
}
|
|
}));
|
|
|
|
const receipt = {
|
|
schema: "guanghu.zhuyuan-persona-daily-dispatch-receipt/v1",
|
|
receipt_id: `ZY-PERSONA-DAILY-${now.replace(/[-:.]/g, "").slice(0, 15)}Z`,
|
|
controller: "ICE-P-ZY001",
|
|
source_repository: "REPO-012",
|
|
source_sha: sourceSha,
|
|
observed_at: now,
|
|
results,
|
|
result: results.some((item) => item.status === "FAILED")
|
|
? "FAILED"
|
|
: "PASS",
|
|
};
|
|
const receiptFile = path.join(
|
|
config.runtime_root,
|
|
"receipts",
|
|
`${receipt.receipt_id}.json`,
|
|
);
|
|
fs.writeFileSync(receiptFile, `${JSON.stringify(receipt, null, 2)}\n`, {
|
|
mode: 0o600,
|
|
});
|
|
if (receipt.result !== "PASS") {
|
|
throw new Error(`daily_dispatch_failed receipt=${receiptFile}`);
|
|
}
|
|
return { ...receipt, receipt_file: receiptFile };
|
|
}
|
|
|
|
async function main() {
|
|
const configFile =
|
|
process.argv[2] ||
|
|
path.join(moduleDir, "fifth-domain-daily.config.server.json");
|
|
const config = loadConfig(configFile);
|
|
const controllerToken = required(
|
|
process.env.PERSONA_TEAM_CONTROLLER_TOKEN,
|
|
"persona_team_controller_token",
|
|
);
|
|
for (const member of config.members) {
|
|
member.controller_token = controllerToken;
|
|
}
|
|
const lockFile = path.join(config.runtime_root, "daily.lock");
|
|
fs.mkdirSync(config.runtime_root, { recursive: true, mode: 0o700 });
|
|
let lock = null;
|
|
try {
|
|
lock = fs.openSync(lockFile, "wx", 0o600);
|
|
fs.writeFileSync(lock, `${process.pid}\n`);
|
|
const receipt = await runDaily(config);
|
|
process.stdout.write(
|
|
`${JSON.stringify({
|
|
event: "zhuyuan_persona_daily_dispatch_complete",
|
|
result: receipt.result,
|
|
source_sha: receipt.source_sha,
|
|
receipt: receipt.receipt_file,
|
|
})}\n`,
|
|
);
|
|
} finally {
|
|
if (lock !== null) fs.closeSync(lock);
|
|
fs.rmSync(lockFile, { force: true });
|
|
}
|
|
}
|
|
|
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
main().catch((error) => {
|
|
process.stderr.write(`${String(error.stack || error)}\n`);
|
|
process.exitCode = 1;
|
|
});
|
|
}
|
|
|
|
export {
|
|
changedContext,
|
|
priorPersonaContext,
|
|
requestObservation,
|
|
runDaily,
|
|
sourceRange,
|
|
verifyMemberResponse,
|
|
};
|