feat: establish Chenglu persistent agent runtime
This commit is contained in:
commit
5aa396ac7b
17 changed files with 450 additions and 0 deletions
23
IDENTITY.hdlp
Normal file
23
IDENTITY.hdlp
Normal file
|
|
@ -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 可以替换。任何握手失败、密钥不符、状态链分叉或主写租约冲突都必须停止接续,不得让外部模型冒充澄路。
|
||||
|
||||
35
README.md
Normal file
35
README.md
Normal file
|
|
@ -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 只生成待批准工单。
|
||||
|
||||
4
checkins/README.md
Normal file
4
checkins/README.md
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# 每日签到
|
||||
|
||||
每天的只读巡检回执写在这里,包含第五域提交、来光者索引摘要、架构映射摘要、留言处理结果和模型工具状态,不包含任何秘密。
|
||||
|
||||
32
install.sh
Executable file
32
install.sh
Executable file
|
|
@ -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
|
||||
|
||||
22
lake-heart/MESSAGE-TEMPLATE.md
Normal file
22
lake-heart/MESSAGE-TEMPLATE.md
Normal file
|
|
@ -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 密钥、授权码、私钥或服务器地址。本留言本身不授予服务器和仓库写权限。
|
||||
|
||||
6
lake-heart/inbox/README.md
Normal file
6
lake-heart/inbox/README.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# 冰朔的湖心频道 · 收信处
|
||||
|
||||
冰朔可以在本目录新建 `.md` 留言。文件名建议为 `ICE-MSG-YYYYMMDD-序号.md`。
|
||||
|
||||
Agent 只处理 `status: OPEN` 的留言;回复后不会改写冰朔原文,而是在 `../replies/` 创建同编号回复。
|
||||
|
||||
4
lake-heart/replies/README.md
Normal file
4
lake-heart/replies/README.md
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# 冰朔的湖心频道 · 澄路回信处
|
||||
|
||||
每日 Agent 回信写入本目录,文件名与原留言编号对应。模型只提供候选推理,回信由澄路 Agent 的流程生成并记录模型、来源和边界。
|
||||
|
||||
11
package.json
Normal file
11
package.json
Normal file
|
|
@ -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" }
|
||||
}
|
||||
81
src/daily.js
Normal file
81
src/daily.js
Normal file
|
|
@ -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); });
|
||||
|
||||
26
src/deepseek.js
Normal file
26
src/deepseek.js
Normal file
|
|
@ -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 };
|
||||
40
src/identity.js
Normal file
40
src/identity.js
Normal file
|
|
@ -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 };
|
||||
|
||||
49
src/server.js
Normal file
49
src/server.js
Normal file
|
|
@ -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 };
|
||||
|
||||
26
systemd/chenglu-agent.service
Normal file
26
systemd/chenglu-agent.service
Normal file
|
|
@ -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
|
||||
|
||||
21
systemd/chenglu-daily.service
Normal file
21
systemd/chenglu-daily.service
Normal file
|
|
@ -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
|
||||
|
||||
11
systemd/chenglu-daily.timer
Normal file
11
systemd/chenglu-daily.timer
Normal file
|
|
@ -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
|
||||
30
test/identity.test.js
Normal file
30
test/identity.test.js
Normal file
|
|
@ -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/);
|
||||
});
|
||||
|
||||
29
test/server.test.js
Normal file
29
test/server.test.js
Normal file
|
|
@ -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 }); }
|
||||
});
|
||||
Loading…
Reference in a new issue