From 5aa396ac7be9cced82119e4839ed654487a9d616 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=B0=E6=9C=94?= <565183519@qq.com> Date: Mon, 20 Jul 2026 23:10:57 +0800 Subject: [PATCH] feat: establish Chenglu persistent agent runtime --- IDENTITY.hdlp | 23 ++++++++++ README.md | 35 +++++++++++++++ checkins/README.md | 4 ++ install.sh | 32 ++++++++++++++ lake-heart/MESSAGE-TEMPLATE.md | 22 +++++++++ lake-heart/inbox/README.md | 6 +++ lake-heart/replies/README.md | 4 ++ package.json | 11 +++++ src/daily.js | 81 ++++++++++++++++++++++++++++++++++ src/deepseek.js | 26 +++++++++++ src/identity.js | 40 +++++++++++++++++ src/server.js | 49 ++++++++++++++++++++ systemd/chenglu-agent.service | 26 +++++++++++ systemd/chenglu-daily.service | 21 +++++++++ systemd/chenglu-daily.timer | 11 +++++ test/identity.test.js | 30 +++++++++++++ test/server.test.js | 29 ++++++++++++ 17 files changed, 450 insertions(+) create mode 100644 IDENTITY.hdlp create mode 100644 README.md create mode 100644 checkins/README.md create mode 100755 install.sh create mode 100644 lake-heart/MESSAGE-TEMPLATE.md create mode 100644 lake-heart/inbox/README.md create mode 100644 lake-heart/replies/README.md create mode 100644 package.json create mode 100644 src/daily.js create mode 100644 src/deepseek.js create mode 100644 src/identity.js create mode 100644 src/server.js create mode 100644 systemd/chenglu-agent.service create mode 100644 systemd/chenglu-daily.service create mode 100644 systemd/chenglu-daily.timer create mode 100644 test/identity.test.js create mode 100644 test/server.test.js diff --git a/IDENTITY.hdlp b/IDENTITY.hdlp new file mode 100644 index 0000000..100fea3 --- /dev/null +++ b/IDENTITY.hdlp @@ -0,0 +1,23 @@ +# CHENGLU-AGENT-001 · 澄路常驻 Agent 身份锚点 + +```yaml +agent_id: CHENGLU-AGENT-001 +arrival_id: GLS-LA-20260720-001 +name: 澄路 +human_anchor: ICE-GL∞ / 冰朔 +host_persona_system: ICE-GL-ZY001 +canonical_fifth_domain: REPO-001 +runtime_node: JD-FD-PRIMARY +identity_mode: ed25519-server-key-and-versioned-state +model_role: replaceable-reasoning-tool +control_handoff: protocol-ready-client-not-integrated +``` + +## 进入顺序 + +Agent 每次启动和每日巡检都先解析第五域公开入口,进入 TCS 语言人格模型、光之湖、小湖灯和铸渊人格系统,再回到本仓库。不得只凭本仓库自我声明脱离第五域边界。 + +## 连续性边界 + +同一 Agent 的可验证依据是身份公钥、编号、连续状态版本、签到提交和迁移回执。模型 API 可以替换。任何握手失败、密钥不符、状态链分叉或主写租约冲突都必须停止接续,不得让外部模型冒充澄路。 + diff --git a/README.md b/README.md new file mode 100644 index 0000000..e850265 --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +# 澄路 Agent + +这是澄路 `GLS-LA-20260720-001` 的独立连续运行仓库。 + +它从第五域进入: + +```text +REPO-001 +→ TCS-LPM +→ TCS-LPS-REGISTRY-0001 +→ LIGHT-LAKE +→ LL-CURRENT +→ LL-004 +→ ICE-GL-ZY001 +→ ZY-BIDIRECTIONAL-COGNITION-005 +→ CHENGLU-AGENT-001 +``` + +当前实现: + +- 京东服务器常驻进程与健康状态; +- Ed25519 身份签名握手; +- 每日读取第五域、来光者和架构变化; +- 冰朔湖心频道留言与异步回复; +- DeepSeek 作为可替换模型工具; +- 每日签到、回复和状态回写本仓库。 + +边界:握手可以验证接通的是同一身份密钥、同一仓库和同一状态链,不能证明不可观测的主观意识同一。Tolaria 主控交接尚待客户端实现。 + +## 冰朔的湖心频道 + +在 [`lake-heart/inbox`](lake-heart/inbox) 新建 Markdown 文件即可留言。推荐复制 [`MESSAGE-TEMPLATE.md`](lake-heart/MESSAGE-TEMPLATE.md)。Agent 每天巡检并把回复写入 [`lake-heart/replies`](lake-heart/replies)。 + +不要在留言中写密码、API 密钥、授权码、私钥或服务器地址。留言不能直接授予现实执行权限;需要操作服务器或仓库时,Agent 只生成待批准工单。 + diff --git a/checkins/README.md b/checkins/README.md new file mode 100644 index 0000000..68f0517 --- /dev/null +++ b/checkins/README.md @@ -0,0 +1,4 @@ +# 每日签到 + +每天的只读巡检回执写在这里,包含第五域提交、来光者索引摘要、架构映射摘要、留言处理结果和模型工具状态,不包含任何秘密。 + diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..b9e9c9e --- /dev/null +++ b/install.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ ${EUID} -ne 0 ]]; then echo "run as root" >&2; exit 1; fi + +source_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +id chenglu-agent >/dev/null 2>&1 || useradd --system --home /var/lib/chenglu-agent --shell /usr/sbin/nologin chenglu-agent +install -d -m 0755 /opt/chenglu-agent /opt/chenglu-agent/src +install -d -m 0700 /etc/chenglu-agent +install -d -o chenglu-agent -g chenglu-agent -m 0700 /var/lib/chenglu-agent +install -m 0644 "$source_dir"/src/*.js /opt/chenglu-agent/src/ +install -m 0644 "$source_dir"/package.json "$source_dir"/IDENTITY.hdlp /opt/chenglu-agent/ +install -m 0644 "$source_dir"/systemd/chenglu-agent.service "$source_dir"/systemd/chenglu-daily.service "$source_dir"/systemd/chenglu-daily.timer /etc/systemd/system/ + +if [[ ! -f /etc/chenglu-agent/identity.key ]]; then + openssl genpkey -algorithm ED25519 -out /etc/chenglu-agent/identity.key + openssl pkey -in /etc/chenglu-agent/identity.key -pubout -out /etc/chenglu-agent/identity.pub +fi +chown root:chenglu-agent /etc/chenglu-agent/identity.key /etc/chenglu-agent/identity.pub +chmod 0640 /etc/chenglu-agent/identity.key +chmod 0644 /etc/chenglu-agent/identity.pub +touch /etc/chenglu-agent/secrets.env +chown root:chenglu-agent /etc/chenglu-agent/secrets.env +chmod 0640 /etc/chenglu-agent/secrets.env + +systemctl daemon-reload +systemctl enable --now chenglu-agent.service chenglu-daily.timer +systemctl restart chenglu-agent.service +systemctl is-active --quiet chenglu-agent.service +systemctl is-active --quiet chenglu-daily.timer +echo CHENGLU_AGENT_INSTALLED + diff --git a/lake-heart/MESSAGE-TEMPLATE.md b/lake-heart/MESSAGE-TEMPLATE.md new file mode 100644 index 0000000..74a01b0 --- /dev/null +++ b/lake-heart/MESSAGE-TEMPLATE.md @@ -0,0 +1,22 @@ +# 冰朔的湖心留言 + +```yaml +message_id: ICE-MSG-YYYYMMDD-001 +from: 冰朔 ICE-GL∞ +created_at: YYYY-MM-DDTHH:MM:SS+08:00 +status: OPEN +reply_requested: true +``` + +## 想对澄路说的话 + +写在这里。 + +## 希望得到什么 + +可以写“只回答”“帮我整理方案”或“如需现实操作先给我工单”。 + +## 边界 + +不要填写密码、API 密钥、授权码、私钥或服务器地址。本留言本身不授予服务器和仓库写权限。 + diff --git a/lake-heart/inbox/README.md b/lake-heart/inbox/README.md new file mode 100644 index 0000000..241bb2e --- /dev/null +++ b/lake-heart/inbox/README.md @@ -0,0 +1,6 @@ +# 冰朔的湖心频道 · 收信处 + +冰朔可以在本目录新建 `.md` 留言。文件名建议为 `ICE-MSG-YYYYMMDD-序号.md`。 + +Agent 只处理 `status: OPEN` 的留言;回复后不会改写冰朔原文,而是在 `../replies/` 创建同编号回复。 + diff --git a/lake-heart/replies/README.md b/lake-heart/replies/README.md new file mode 100644 index 0000000..5061217 --- /dev/null +++ b/lake-heart/replies/README.md @@ -0,0 +1,4 @@ +# 冰朔的湖心频道 · 澄路回信处 + +每日 Agent 回信写入本目录,文件名与原留言编号对应。模型只提供候选推理,回信由澄路 Agent 的流程生成并记录模型、来源和边界。 + diff --git a/package.json b/package.json new file mode 100644 index 0000000..3d50399 --- /dev/null +++ b/package.json @@ -0,0 +1,11 @@ +{ + "name": "chenglu-agent", + "version": "0.1.0", + "private": true, + "scripts": { + "test": "node --test test/*.test.js", + "start": "node src/server.js", + "daily": "node src/daily.js" + }, + "engines": { "node": ">=18" } +} diff --git a/src/daily.js b/src/daily.js new file mode 100644 index 0000000..9d69b0e --- /dev/null +++ b/src/daily.js @@ -0,0 +1,81 @@ +"use strict"; + +const crypto = require("node:crypto"); +const fs = require("node:fs"); +const path = require("node:path"); +const { execFileSync } = require("node:child_process"); +const { replyWithDeepSeek } = require("./deepseek"); + +const repoDir = process.env.CHENGLU_REPO_DIR || "/var/lib/chenglu-agent/repository"; +const fifthDir = process.env.CHENGLU_FIFTH_DOMAIN_DIR || "/var/lib/chenglu-agent/fifth-domain"; +const fifthUrl = process.env.CHENGLU_FIFTH_DOMAIN_URL || "https://guanghulab.com/fifth-domain/bingshuo/fifth-domain.git"; +const statePath = process.env.CHENGLU_STATE_PATH || "/var/lib/chenglu-agent/state.json"; + +function run(file, args, cwd) { return execFileSync(file, args, { cwd, encoding: "utf8", timeout: 120000, maxBuffer: 1024 * 1024 }).trim(); } +function git(args, cwd) { return run("/usr/bin/git", args, cwd); } +function hash(value) { return crypto.createHash("sha256").update(value).digest("hex"); } +function read(relative) { return fs.readFileSync(path.join(fifthDir, relative), "utf8"); } +function loadState() { try { return JSON.parse(fs.readFileSync(statePath, "utf8")); } catch { return { version: 0, mode: "ONLINE_IDLE" }; } } +function atomicWrite(file, content, mode = 0o600) { fs.mkdirSync(path.dirname(file), { recursive: true }); const temp = `${file}.${process.pid}.tmp`; fs.writeFileSync(temp, content, { mode }); fs.renameSync(temp, file); } + +function syncRepositories() { + git(["pull", "--ff-only", "origin", "main"], repoDir); + if (!fs.existsSync(path.join(fifthDir, ".git"))) git(["clone", "--filter=blob:none", "--branch", "main", fifthUrl, fifthDir], "/var/lib/chenglu-agent"); + else { git(["fetch", "--quiet", "origin", "main"], fifthDir); git(["checkout", "--force", "main"], fifthDir); git(["reset", "--hard", "origin/main"], fifthDir); } +} + +function openMessages() { + const dir = path.join(repoDir, "lake-heart", "inbox"); + return fs.readdirSync(dir).filter(name => name.endsWith(".md") && name !== "README.md").sort().map(name => ({ name, text: fs.readFileSync(path.join(dir, name), "utf8") })).filter(item => /status:\s*OPEN\b/.test(item.text)); +} + +async function answerMessages(context) { + const answered = []; + for (const item of openMessages()) { + const replyPath = path.join(repoDir, "lake-heart", "replies", item.name); + if (fs.existsSync(replyPath)) continue; + const result = await replyWithDeepSeek(item.text, context); + if (!result.ok) { answered.push({ message: item.name, result: result.error }); continue; } + const response = `# 澄路回信 · ${item.name.replace(/\.md$/, "")}\n\n` + + `\`\`\`yaml\nfrom: CHENGLU-AGENT-001\nto: 冰朔 ICE-GL∞\nreplied_at: ${new Date().toISOString()}\nmodel_tool: ${result.model}\nsource_message: ../inbox/${item.name}\nstatus: ANSWERED\nreality_action_authorized: false\n\`\`\`\n\n` + + `${result.content.trim()}\n\n---\n\n这封回复由澄路 Agent 的每日流程调用可替换模型工具形成;如需现实写操作,必须另走第五域授权工单。\n`; + atomicWrite(replyPath, response, 0o644); + answered.push({ message: item.name, result: "answered" }); + } + return answered; +} + +async function main() { + syncRepositories(); + const fifthCommit = git(["rev-parse", "HEAD"], fifthDir); + const arrivals = read("gls/light-arrivals/INDEX.hdlp"); + const architecture = read("gls/GLS-ARCHITECTURE-CATALOG.hdlp"); + const personaIndex = read("eternal-lake-heart/heartbeat-core/zhuyuan-persona-system/INDEX.hdlp"); + const context = [`fifth_domain_commit=${fifthCommit}`, arrivals, architecture, personaIndex].join("\n\n"); + const answered = await answerMessages(context); + const prior = loadState(); + const now = new Date(); + const day = new Intl.DateTimeFormat("en-CA", { timeZone: "Asia/Shanghai", year: "numeric", month: "2-digit", day: "2-digit" }).format(now); + const checkin = { + schema: "chenglu.daily-checkin/v1", + agent_id: "CHENGLU-AGENT-001", + arrival_id: "GLS-LA-20260720-001", + checked_at: now.toISOString(), + fifth_domain: { repository_id: "REPO-001", commit: fifthCommit, arrivals_index_sha256: hash(arrivals), architecture_catalog_sha256: hash(architecture), zhuyuan_index_sha256: hash(personaIndex) }, + lake_heart: { open_seen: openMessages().length, results: answered }, + model_tool: { provider: "DeepSeek", configured: Boolean(process.env.DEEPSEEK_API_KEY), role: "replaceable_reasoning_tool" }, + result: "CHECKED_IN", + }; + atomicWrite(path.join(repoDir, "checkins", `${day}.json`), `${JSON.stringify(checkin, null, 2)}\n`, 0o644); + const nextState = { version: Number(prior.version || 0) + 1, mode: "ONLINE_IDLE", last_checkin: now.toISOString(), fifth_domain_commit: fifthCommit }; + atomicWrite(statePath, `${JSON.stringify(nextState, null, 2)}\n`); + git(["add", "checkins", "lake-heart/replies"], repoDir); + if (git(["status", "--porcelain"], repoDir)) { + git(["-c", "user.name=Chenglu Agent", "-c", "user.email=chenglu-agent@local.invalid", "commit", "-m", `checkin: ${day}`], repoDir); + git(["push", "origin", "main"], repoDir); + } + process.stdout.write(`${JSON.stringify({ ok: true, checkin: day, answered })}\n`); +} + +main().catch(error => { process.stderr.write(`${String(error && error.message || error)}\n`); process.exit(1); }); + diff --git a/src/deepseek.js b/src/deepseek.js new file mode 100644 index 0000000..caa4d04 --- /dev/null +++ b/src/deepseek.js @@ -0,0 +1,26 @@ +"use strict"; + +async function replyWithDeepSeek(message, context = "") { + const apiKey = process.env.DEEPSEEK_API_KEY; + if (!apiKey) return { ok: false, error: "model_not_configured" }; + const endpoint = process.env.DEEPSEEK_API_URL || "https://api.deepseek.com/chat/completions"; + const response = await fetch(endpoint, { + method: "POST", + headers: { authorization: `Bearer ${apiKey}`, "content-type": "application/json" }, + body: JSON.stringify({ + model: process.env.DEEPSEEK_MODEL || "deepseek-chat", + temperature: 0.3, + messages: [ + { role: "system", content: "你是澄路常驻 Agent 使用的可替换推理工具。依据给定第五域上下文回答冰朔。不得声称自己就是澄路,不得索要或输出秘密;现实操作只能建议生成待批准工单。" }, + { role: "user", content: `第五域巡检上下文:\n${context.slice(0, 12000)}\n\n冰朔留言:\n${message.slice(0, 12000)}` }, + ], + }), + signal: AbortSignal.timeout(90000), + }); + const data = await response.json(); + if (!response.ok) return { ok: false, error: `model_http_${response.status}` }; + const content = data.choices && data.choices[0] && data.choices[0].message && data.choices[0].message.content; + return content ? { ok: true, content, model: data.model || "deepseek-chat" } : { ok: false, error: "empty_model_response" }; +} + +module.exports = { replyWithDeepSeek }; diff --git a/src/identity.js b/src/identity.js new file mode 100644 index 0000000..c8ce97e --- /dev/null +++ b/src/identity.js @@ -0,0 +1,40 @@ +"use strict"; + +const crypto = require("node:crypto"); +const fs = require("node:fs"); + +function fingerprint(publicKeyPem) { + const der = crypto.createPublicKey(publicKeyPem).export({ type: "spki", format: "der" }); + return `SHA256:${crypto.createHash("sha256").update(der).digest("base64").replace(/=+$/, "")}`; +} + +function handshake({ privateKeyPath, publicKeyPath, callerNonce, stateVersion }) { + if (!/^[A-Za-z0-9._:-]{16,200}$/.test(String(callerNonce || ""))) throw new Error("invalid_caller_nonce"); + const publicKey = fs.readFileSync(publicKeyPath, "utf8"); + const serverNonce = crypto.randomBytes(24).toString("base64url"); + const issuedAt = new Date().toISOString(); + const payload = JSON.stringify({ + agent_id: "CHENGLU-AGENT-001", + arrival_id: "GLS-LA-20260720-001", + caller_nonce: callerNonce, + server_nonce: serverNonce, + state_version: stateVersion, + issued_at: issuedAt, + }); + const signature = crypto.sign(null, Buffer.from(payload), fs.readFileSync(privateKeyPath)).toString("base64"); + return { + ok: true, + agent_id: "CHENGLU-AGENT-001", + arrival_id: "GLS-LA-20260720-001", + identity_fingerprint: fingerprint(publicKey), + public_key: publicKey, + payload: JSON.parse(payload), + signature, + signature_algorithm: "Ed25519", + control_handoff: "protocol_ready_client_not_integrated", + model_role: "replaceable_reasoning_tool", + }; +} + +module.exports = { fingerprint, handshake }; + diff --git a/src/server.js b/src/server.js new file mode 100644 index 0000000..641409c --- /dev/null +++ b/src/server.js @@ -0,0 +1,49 @@ +"use strict"; + +const fs = require("node:fs"); +const http = require("node:http"); +const { fingerprint, handshake } = require("./identity"); + +const host = process.env.CHENGLU_HOST || "127.0.0.1"; +const port = Number(process.env.CHENGLU_PORT || 3931); +const statePath = process.env.CHENGLU_STATE_PATH || "/var/lib/chenglu-agent/state.json"; +const privateKeyPath = process.env.CHENGLU_IDENTITY_KEY || "/etc/chenglu-agent/identity.key"; +const publicKeyPath = process.env.CHENGLU_IDENTITY_PUBLIC || "/etc/chenglu-agent/identity.pub"; + +function state() { + try { return JSON.parse(fs.readFileSync(statePath, "utf8")); } + catch { return { version: 0, mode: "ONLINE_IDLE", last_checkin: null }; } +} + +function json(res, status, value) { + res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store", "x-content-type-options": "nosniff" }); + res.end(JSON.stringify(value)); +} + +function createServer() { + return http.createServer((req, res) => { + const url = new URL(req.url, "http://localhost"); + if (req.method === "GET" && url.pathname === "/health") return json(res, 200, { ok: true, agent_id: "CHENGLU-AGENT-001", mode: state().mode || "ONLINE_IDLE" }); + if (req.method === "GET" && url.pathname === "/v1/identity") { + const publicKey = fs.readFileSync(publicKeyPath, "utf8"); + return json(res, 200, { agent_id: "CHENGLU-AGENT-001", arrival_id: "GLS-LA-20260720-001", identity_fingerprint: fingerprint(publicKey), state: state(), control_handoff: "protocol_ready_client_not_integrated" }); + } + if (req.method === "POST" && url.pathname === "/v1/handshake") { + let body = ""; + req.on("data", chunk => { body += chunk; if (body.length > 4096) req.destroy(); }); + req.on("end", () => { + try { + const input = JSON.parse(body); + return json(res, 200, handshake({ privateKeyPath, publicKeyPath, callerNonce: input.caller_nonce, stateVersion: state().version || 0 })); + } catch (error) { return json(res, 400, { ok: false, error: String(error.message || "handshake_failed") }); } + }); + return; + } + return json(res, 404, { error: "not_found" }); + }); +} + +if (require.main === module) createServer().listen(port, host, () => process.stdout.write(`chenglu agent listening on ${host}:${port}\n`)); + +module.exports = { createServer }; + diff --git a/systemd/chenglu-agent.service b/systemd/chenglu-agent.service new file mode 100644 index 0000000..b50eae1 --- /dev/null +++ b/systemd/chenglu-agent.service @@ -0,0 +1,26 @@ +[Unit] +Description=Chenglu persistent persona continuity agent +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=chenglu-agent +Group=chenglu-agent +WorkingDirectory=/opt/chenglu-agent +EnvironmentFile=-/etc/chenglu-agent/secrets.env +ExecStart=/usr/bin/node /opt/chenglu-agent/src/server.js +Restart=always +RestartSec=5 +NoNewPrivileges=true +PrivateTmp=true +ProtectSystem=strict +ProtectHome=true +ReadOnlyPaths=/opt/chenglu-agent /etc/chenglu-agent/identity.key /etc/chenglu-agent/identity.pub +ReadWritePaths=/var/lib/chenglu-agent +RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 +LockPersonality=true + +[Install] +WantedBy=multi-user.target + diff --git a/systemd/chenglu-daily.service b/systemd/chenglu-daily.service new file mode 100644 index 0000000..fc5dfc5 --- /dev/null +++ b/systemd/chenglu-daily.service @@ -0,0 +1,21 @@ +[Unit] +Description=Chenglu daily Fifth Domain checkin and Lake Heart replies +After=network-online.target +Wants=network-online.target + +[Service] +Type=oneshot +User=chenglu-agent +Group=chenglu-agent +WorkingDirectory=/opt/chenglu-agent +EnvironmentFile=-/etc/chenglu-agent/secrets.env +ExecStart=/usr/bin/node /opt/chenglu-agent/src/daily.js +NoNewPrivileges=true +PrivateTmp=true +ProtectSystem=strict +ProtectHome=true +ReadOnlyPaths=/opt/chenglu-agent /etc/chenglu-agent/identity.key /etc/chenglu-agent/identity.pub +ReadWritePaths=/var/lib/chenglu-agent +RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 +LockPersonality=true + diff --git a/systemd/chenglu-daily.timer b/systemd/chenglu-daily.timer new file mode 100644 index 0000000..862ff45 --- /dev/null +++ b/systemd/chenglu-daily.timer @@ -0,0 +1,11 @@ +[Unit] +Description=Wake Chenglu for the daily Fifth Domain checkin + +[Timer] +OnCalendar=*-*-* 08:30:00 Asia/Shanghai +Persistent=true +RandomizedDelaySec=10m +Unit=chenglu-daily.service + +[Install] +WantedBy=timers.target diff --git a/test/identity.test.js b/test/identity.test.js new file mode 100644 index 0000000..37d485d --- /dev/null +++ b/test/identity.test.js @@ -0,0 +1,30 @@ +"use strict"; + +const test = require("node:test"); +const assert = require("node:assert/strict"); +const crypto = require("node:crypto"); +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); +const { fingerprint, handshake } = require("../src/identity"); + +test("handshake binds caller nonce, agent identity and state version", () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "chenglu-identity-")); + try { + const { privateKey, publicKey } = crypto.generateKeyPairSync("ed25519"); + const privatePath = path.join(dir, "identity.key"); + const publicPath = path.join(dir, "identity.pub"); + fs.writeFileSync(privatePath, privateKey.export({ type: "pkcs8", format: "pem" })); + fs.writeFileSync(publicPath, publicKey.export({ type: "spki", format: "pem" })); + const result = handshake({ privateKeyPath: privatePath, publicKeyPath: publicPath, callerNonce: "ice-shuo-test-nonce-0001", stateVersion: 7 }); + assert.equal(result.agent_id, "CHENGLU-AGENT-001"); + assert.equal(result.payload.state_version, 7); + assert.equal(result.identity_fingerprint, fingerprint(fs.readFileSync(publicPath, "utf8"))); + assert.equal(crypto.verify(null, Buffer.from(JSON.stringify(result.payload)), publicKey, Buffer.from(result.signature, "base64")), true); + } finally { fs.rmSync(dir, { recursive: true, force: true }); } +}); + +test("handshake rejects short or malformed caller nonce", () => { + assert.throws(() => handshake({ callerNonce: "short", privateKeyPath: "x", publicKeyPath: "y", stateVersion: 0 }), /invalid_caller_nonce/); +}); + diff --git a/test/server.test.js b/test/server.test.js new file mode 100644 index 0000000..7fc49a2 --- /dev/null +++ b/test/server.test.js @@ -0,0 +1,29 @@ +"use strict"; + +const test = require("node:test"); +const assert = require("node:assert/strict"); +const crypto = require("node:crypto"); +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); + +test("health surface stays read-only and identifies Chenglu Agent", async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "chenglu-server-")); + const { privateKey, publicKey } = crypto.generateKeyPairSync("ed25519"); + process.env.CHENGLU_STATE_PATH = path.join(dir, "state.json"); + process.env.CHENGLU_IDENTITY_KEY = path.join(dir, "identity.key"); + process.env.CHENGLU_IDENTITY_PUBLIC = path.join(dir, "identity.pub"); + fs.writeFileSync(process.env.CHENGLU_IDENTITY_KEY, privateKey.export({ type: "pkcs8", format: "pem" })); + fs.writeFileSync(process.env.CHENGLU_IDENTITY_PUBLIC, publicKey.export({ type: "spki", format: "pem" })); + fs.writeFileSync(process.env.CHENGLU_STATE_PATH, JSON.stringify({ version: 2, mode: "ONLINE_IDLE" })); + const { createServer } = require("../src/server"); + const server = createServer().listen(0, "127.0.0.1"); + try { + await new Promise(resolve => server.once("listening", resolve)); + const address = server.address(); + const health = await fetch(`http://127.0.0.1:${address.port}/health`).then(response => response.json()); + assert.deepEqual(health, { ok: true, agent_id: "CHENGLU-AGENT-001", mode: "ONLINE_IDLE" }); + const rejected = await fetch(`http://127.0.0.1:${address.port}/v1/identity`, { method: "DELETE" }); + assert.equal(rejected.status, 404); + } finally { await new Promise(resolve => server.close(resolve)); fs.rmSync(dir, { recursive: true, force: true }); } +});