feat(authz): add resident agent operation receipts
This commit is contained in:
parent
100848e9e4
commit
357ac5e67a
6 changed files with 136 additions and 18 deletions
|
|
@ -112,6 +112,8 @@ function createApp(options = {}) {
|
|||
targets: [...targets],
|
||||
scopes: actions,
|
||||
owner_handoff: "open request_url and request pre-registered mailbox verification",
|
||||
workflow: ["create_workorder", "owner_handoff", "claim_session", "read_navigation_map", "ack_navigation_map", "check_session_status", "execute_registered_action", "read_operation_receipt"],
|
||||
diagnostics: diagnosticCatalog(),
|
||||
approval_ttl: manager.approvalTtl,
|
||||
session_ttl: manager.sessionTtl,
|
||||
max_session_lifetime: manager.maxSessionLifetime,
|
||||
|
|
@ -168,6 +170,7 @@ function createApp(options = {}) {
|
|||
request_url: `${publicBaseUrl}/request/${created.handoffToken}`,
|
||||
expires_in: created.expiresIn,
|
||||
status: "waiting_for_owner_handoff",
|
||||
receipt: receipt({ state: "waiting_for_owner_handoff", diagnostic_code: "owner_handoff_required", workorder_id: created.id, next_step: "把 request_url 交给主人打开一次;页面会向预登记邮箱发送批准链接。不要索要密码、验证码或令牌。" }),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -191,7 +194,20 @@ function createApp(options = {}) {
|
|||
const token = bearer(req);
|
||||
const claimed = manager.claim(claimMatch[1], token);
|
||||
if (!claimed.ok) return json(res, claimed.reason === "approval_pending" ? 202 : 403, { error: claimed.reason });
|
||||
return json(res, 200, { ok: true, session_token: claimed.sessionToken, expires_in: claimed.expiresIn, target: claimed.target, scope: claimed.scope, action: claimed.action, resource: claimed.resource || "" });
|
||||
return json(res, 200, { ok: true, session_token: claimed.sessionToken, expires_in: claimed.expiresIn, target: claimed.target, scope: claimed.scope, action: claimed.action, resource: claimed.resource || "", receipt: claimed.receipt || receipt({ state: "session_issued", diagnostic_code: "session_issued", workorder_id: claimed.workorderId, next_step: "读取并确认实时导航图。" }) });
|
||||
}
|
||||
|
||||
if (req.method === "POST" && url.pathname === "/api/session/status") {
|
||||
const body = await readJson(req);
|
||||
if (!body) return json(res, 400, failure("invalid_json"));
|
||||
const token = bearer(req);
|
||||
const target = String(body.target || "");
|
||||
const scope = String(body.scope || "");
|
||||
const verified = manager.verifySession(token, { pid: String(body.persona_id || "") }, target, scope, "read-navigation-map");
|
||||
if (!verified.ok) return json(res, 403, failure(verified.reason));
|
||||
const map = mapGate.read(target);
|
||||
const mapVerified = mapGate.verify(token, target, map.hash);
|
||||
return json(res, 200, { ok: true, state: mapVerified.ok ? "ready_to_execute" : "map_ack_required", workorder_id: verified.session.workorderId || "", target, scope, allowed_actions: verified.session.actions || [verified.session.action], expires_at: verified.session.expiresAt, map: { hash: map.hash, acknowledged: mapVerified.ok }, last_receipt: verified.session.lastReceipt || null, next_step: mapVerified.ok ? "只执行 allowed_actions 中已登记的动作;每次执行后读取 operation receipt。" : "先读取 /api/navigation-map/read,再提交同一 map_hash 至 /api/navigation-map/ack。" });
|
||||
}
|
||||
|
||||
if (req.method === "POST" && url.pathname === "/api/session/verify") {
|
||||
|
|
@ -238,47 +254,54 @@ function createApp(options = {}) {
|
|||
const verified = manager.verifySession(token, { pid: String(body.persona_id || "") }, String(body.target || ""), String(body.scope || ""), "read-navigation-map");
|
||||
if (!verified.ok) return json(res, 403, { error: verified.reason });
|
||||
const acked = mapGate.ack(token, String(body.target || ""), String(body.map_hash || ""), Date.now() / 1000, Math.max(1, verified.session.expiresAt - Date.now() / 1000));
|
||||
return json(res, acked.ok ? 200 : 409, acked.ok ? { ok: true, target: body.target, map_hash: body.map_hash } : { error: acked.reason });
|
||||
if (!acked.ok) return json(res, 409, failure(acked.reason));
|
||||
const operationReceipt = receipt({ state: "map_acknowledged", diagnostic_code: "map_acknowledged", workorder_id: verified.session.workorderId, target: body.target, next_step: "可查询 session/status,再执行本会话 allowed_actions 内的固定动作。" });
|
||||
manager.recordReceipt(token, operationReceipt);
|
||||
return json(res, 200, { ok: true, target: body.target, map_hash: body.map_hash, receipt: operationReceipt });
|
||||
}
|
||||
|
||||
if (req.method === "POST" && url.pathname === "/api/actions/execute") {
|
||||
const body = await readJson(req);
|
||||
if (!body) return json(res, 400, { error: "invalid_json" });
|
||||
if (body.cmd || body.command || body.shell || body.args) return json(res, 400, { error: "arbitrary_command_forbidden" });
|
||||
if (!body) return json(res, 400, failure("invalid_json"));
|
||||
if (body.cmd || body.command || body.shell || body.args) return json(res, 400, failure("arbitrary_command_forbidden"));
|
||||
const token = bearer(req);
|
||||
const target = String(body.target || "");
|
||||
const scope = String(body.scope || "");
|
||||
const action = String(body.action || "");
|
||||
const resource = String(body.resource || "");
|
||||
const verified = manager.verifySession(token, { pid: String(body.persona_id || "") }, target, scope, action, Date.now() / 1000, resource);
|
||||
if (!verified.ok) return json(res, 403, { error: verified.reason });
|
||||
if (!verified.ok) return json(res, 403, failure(verified.reason));
|
||||
const map = mapGate.read(target);
|
||||
const mapVerified = mapGate.verify(token, target, map.hash);
|
||||
if (!mapVerified.ok) return json(res, 423, { error: mapVerified.reason, required_action: "read-navigation-map" });
|
||||
if (!mapVerified.ok) return json(res, 423, failure(mapVerified.reason, "先读取并确认导航图。", { required_action: "read-navigation-map" }));
|
||||
const result = await executeAction(resource ? { action, target, resource } : { action, target });
|
||||
return json(res, result.ok ? 200 : 502, result);
|
||||
const operationReceipt = receipt({ state: result.ok ? "succeeded" : "failed", diagnostic_code: result.ok ? "action_succeeded" : String(result.error || "action_execution_failed"), workorder_id: verified.session.workorderId, target, action, evidence: safeEvidence(result), next_step: result.ok ? "读取 session/status 确认当前回执;如需新范围、目标或资源,重新发起工单。" : "读取 diagnostic_code 与 evidence;仅按 next_step 修复,不要切换到其他服务器或猜测凭证。" });
|
||||
manager.recordReceipt(token, operationReceipt);
|
||||
return json(res, result.ok ? 200 : 502, { ...result, receipt: operationReceipt });
|
||||
}
|
||||
|
||||
if (req.method === "POST" && url.pathname === "/api/repo-push/grant") {
|
||||
const body = await readJson(req);
|
||||
if (!body) return json(res, 400, { error: "invalid_json" });
|
||||
if (!body) return json(res, 400, failure("invalid_json"));
|
||||
const token = bearer(req);
|
||||
const target = String(body.target || "");
|
||||
const scope = String(body.scope || "repo-push");
|
||||
const repo = String(body.repo || "").toLowerCase();
|
||||
if (!/^bingshuo\/[a-z0-9._-]+$/.test(repo)) return json(res, 400, { error: "repo_not_allowlisted" });
|
||||
if (!/^bingshuo\/[a-z0-9._-]+$/.test(repo)) return json(res, 400, failure("repo_not_allowlisted"));
|
||||
const verified = manager.verifySession(token, { pid: String(body.persona_id || "") }, target, scope, "push-repository");
|
||||
if (!verified.ok) return json(res, 403, { error: verified.reason });
|
||||
if (!verified.ok) return json(res, 403, failure(verified.reason));
|
||||
const map = mapGate.read(target);
|
||||
const mapVerified = mapGate.verify(token, target, map.hash);
|
||||
if (!mapVerified.ok) return json(res, 423, { error: mapVerified.reason, required_action: "read-navigation-map" });
|
||||
if (!mapVerified.ok) return json(res, 423, failure(mapVerified.reason, "先读取并确认导航图。", { required_action: "read-navigation-map" }));
|
||||
fs.mkdirSync(repoGrantDir, { recursive: true, mode: 0o2770 });
|
||||
const grant = { schema: "guanghu.repo-push-grant/v1", repo, target, persona_id: body.persona_id, map_hash: map.hash, issued_at: Date.now() / 1000, expires_at: verified.session.expiresAt };
|
||||
const grantFile = path.join(repoGrantDir, `${repo.replace("/", "__")}.json`);
|
||||
const temp = `${grantFile}.${process.pid}.tmp`;
|
||||
fs.writeFileSync(temp, JSON.stringify(grant), { mode: 0o640 });
|
||||
fs.renameSync(temp, grantFile);
|
||||
return json(res, 200, { ok: true, repo, target, expires_at: grant.expires_at });
|
||||
const operationReceipt = receipt({ state: "blocked", diagnostic_code: "repo_push_transport_unavailable", workorder_id: verified.session.workorderId, target, action: "push-repository", next_step: "服务器已登记本次推送许可,但安全推送接收器尚未部署;不要重试裸 git push、不要索要账号密码。等待受限 bundle 接收器上线后按同一工单回执执行。" });
|
||||
manager.recordReceipt(token, operationReceipt);
|
||||
return json(res, 200, { ok: true, repo, target, expires_at: grant.expires_at, transport: { status: "not_configured", diagnostic_code: "repo_push_transport_unavailable", next_step: operationReceipt.next_step }, receipt: operationReceipt });
|
||||
}
|
||||
|
||||
return json(res, 404, { error: "not_found" });
|
||||
|
|
@ -386,6 +409,25 @@ function clientAddress(req) {
|
|||
const forwarded = String(req.headers["x-forwarded-for"] || "").split(",").map(value => value.trim()).filter(Boolean);
|
||||
return String(forwarded[forwarded.length - 1] || req.socket.remoteAddress || "unknown").slice(0, 96);
|
||||
}
|
||||
function receipt({ state, diagnostic_code, workorder_id = "", target = "", action = "", evidence = null, next_step = "" }) {
|
||||
return { schema: "guanghu.operation-receipt/v1", state, diagnostic_code, workorder_id, target, action, occurred_at: Date.now() / 1000, ...(evidence ? { evidence } : {}), next_step };
|
||||
}
|
||||
function safeEvidence(result) {
|
||||
const clip = value => String(value || "").replace(/(password|token|secret|authorization)\s*[:=]\s*\S+/gi, "$1=[redacted]").slice(0, 1200);
|
||||
return { exit_code: Number.isInteger(result.exit_code) ? result.exit_code : null, stdout: clip(result.stdout), stderr: clip(result.stderr) };
|
||||
}
|
||||
function failure(error, next_step = "读取 diagnostic_code;按 next_step 处理,勿猜测凭证或切换服务器。", extra = {}) {
|
||||
return { ok: false, error, receipt: receipt({ state: "blocked", diagnostic_code: error, next_step }), ...extra };
|
||||
}
|
||||
function diagnosticCatalog() {
|
||||
return {
|
||||
owner_handoff_required: "工单已创建,等待主人打开申请页并完成预登记邮箱批准。",
|
||||
map_ack_required: "会话有效,但尚未确认此目标节点的实时导航图。",
|
||||
action_execution_failed: "服务器固定动作已执行但失败;回执会包含受限证据与下一步。",
|
||||
repo_push_transport_unavailable: "许可已登记,但安全推送接收器尚未部署,禁止把它误判为 git 凭证。",
|
||||
session_expired: "会话已过期;以同一目标和范围重新申请工单。",
|
||||
};
|
||||
}
|
||||
function validateWorkorderBody(body, targets, actions) {
|
||||
if (body.email || body.recipient || body.smtp_pass) return { ok: false, status: 400, error: "direct_recipient_forbidden" };
|
||||
if (!body.persona_id || !body.target || !body.scope || !body.action) return { ok: false, status: 400, error: "missing_required_field" };
|
||||
|
|
|
|||
Loading…
Reference in a new issue