arcrun — AI workflow execution engine (clean history)

Self-hosted 開源:WASM 零件 + recipe + cypher-executor,跑在你自己的 Cloudflare。

此為重建的乾淨歷史起點(移除曾誤 commit 的 GCP SA 金鑰,舊歷史保留在
richblack/arcrun 與本地 backup 分支)。含:
- acr init --self-hosted installer(建 KV/R2 + codeload 拉預編譯 wasm + wrangler deploy + seed recipe)
- recipe push 把關(資料外流提醒 + 打通檢查)
- 19 個正當零件預編譯 wasm(claude_api/km_writer/kbdb_upsert_block 排除:違反 DECISIONS §1)
- CLI / cypher-executor / registry / 完整 SDD

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
uncle6me-web
2026-06-03 15:52:38 +08:00
commit 922a57fe34
485 changed files with 89356 additions and 0 deletions
+127
View File
@@ -0,0 +1,127 @@
/**
* 資料外流警示 — CLI 互動(data-exfil-warning SDD §1a / B
*
* 觸發策略:只在「資料變成可被外部呼叫」時警示(webhook 部署 / recipe push)。
* 互動形式(richblack):仿 GCP 刪 project —— 要用戶打資源名證明讀了警示(比 y/n 硬,不用打一大串)。
* 同意 = 法律憑證:回傳的 ExposureConsent 帶 understood(用戶打的內容)+ 時間,server 端 log。
* 誠實限制:非 TTYAI 直跑)無 --confirm-exposure → 拒絕(AI 不該替人類確認暴露)。
*/
import { createInterface } from 'node:readline/promises';
import chalk from 'chalk';
import { loadConfig, saveConfig } from './config.js';
export interface ExposureConsent {
confirmed_by_human: true;
understood: string;
confirmed_at: string;
suppress_future?: boolean;
}
// 註(2026-05-30 信任修正):移除 --confirm-exposure / --suppress-warning 旗標。
// 理由:arcrun 是 AI 的工具,AI 自己能加旗標 = 自己批准自己 = 閘門虛設(違 DECISIONS §7)。
// 唯一通過 = 人類在 TTY 互動輸入資源名(AI 非互動環境生不出)。「以後不再問」改成互動中詢問。
export interface ExposureWarningOptions {
// 預留:未來 CI 用「人類預先簽的 token」(非 AI 能生的 flag)。第一期不做。
_reserved?: never;
}
export interface ExposureContext {
/** 動作種類,顯示用:'webhook' | 'recipe' */
kind: string;
/** 資源名(用戶要打這個字確認)*/
resourceName: string;
/** 暴露後的 URL / 去向(顯示用,可選) */
destination?: string;
/** 這個資源讀取/送出什麼(盡力盤,盤不出傳 undefined) */
dataSummary?: string;
}
/**
* 取得暴露同意。回傳 ExposureConsent(放進 push 請求 body)。
* 未取得同意 → 印訊息並 return null(呼叫端應中止)。
*/
export async function obtainExposureConsent(
ctx: ExposureContext,
opts: ExposureWarningOptions = {},
): Promise<ExposureConsent | null> {
const nowIso = new Date().toISOString();
const memKey = `${ctx.kind}:${ctx.resourceName}`;
// §3 首次問記住:本機已記錄同意此資源 → 不重問(server 端仍存法律憑證並強制)。
const cfg = loadConfig();
const prior = cfg.exposure_consented?.[memKey];
if (prior) {
return {
confirmed_by_human: true,
understood: `先前已同意暴露 ${ctx.resourceName}${prior.confirmed_at}${prior.suppress_future ? ',已選不再警示' : ''}`,
confirmed_at: prior.confirmed_at,
suppress_future: prior.suppress_future,
};
}
// 非 TTY(AI 直跑)→ 一律拒絕,無捷徑。AI 不該、也不能替人類確認暴露。
// (移除了 --confirm-exposure 旗標:那是 AI 自己能加的後門,等於自己批准自己。)
if (!process.stdin.isTTY) {
console.error(chalk.red('\n⚠️ 此動作會把資源變成可被外部呼叫(暴露/送出資料),需人類明示同意。'));
console.error(chalk.gray(' 你(AI)無法確認暴露——這必須由人類在終端機親自執行、輸入資源名確認。'));
console.error(chalk.gray(' 請把這件事交給人類做。\n'));
return null;
}
// 互動式警示 + 打資源名確認(唯一通過路徑,AI 生不出這個輸入)
printWarning(ctx);
const rl = createInterface({ input: process.stdin, output: process.stdout });
try {
const answer = (await rl.question(
chalk.bold(` 確認暴露?請輸入資源名 "${ctx.resourceName}" 以繼續(或 Ctrl-C 取消):`),
)).trim();
if (answer !== ctx.resourceName) {
console.error(chalk.red(`\n 輸入不符(需輸入 "${ctx.resourceName}")。已取消,未暴露。\n`));
return null;
}
// 互動中詢問「以後不再問」(人類選,不是 AI 加旗標)
const suppressAns = (await rl.question(
chalk.gray(` 以後此資源(${ctx.resourceName})的暴露不再提醒?(y/N)`),
)).trim().toLowerCase();
const suppress = suppressAns === 'y' || suppressAns === 'yes';
rememberConsent(memKey, nowIso, suppress);
return {
confirmed_by_human: true,
understood: `用戶輸入資源名 "${ctx.resourceName}" 確認暴露${ctx.destination ? `(去向:${ctx.destination}` : ''}${suppress ? ';並選擇以後不再提醒' : ''}`,
confirmed_at: nowIso,
suppress_future: suppress,
};
} finally {
rl.close();
}
}
/** 本機記住此資源已同意(避免下次重問;server 端仍獨立存法律憑證並強制) */
function rememberConsent(memKey: string, confirmedAt: string, suppressFuture: boolean): void {
try {
const cfg = loadConfig();
cfg.exposure_consented = cfg.exposure_consented ?? {};
cfg.exposure_consented[memKey] = { confirmed_at: confirmedAt, suppress_future: suppressFuture };
saveConfig(cfg);
} catch {
// 記不住不影響本次同意(server 端仍會擋首次)
}
}
function printWarning(ctx: ExposureContext): void {
console.log(chalk.yellow.bold(`\n⚠️ 資料外流警示`));
console.log(chalk.yellow(` 這個動作會把 ${ctx.kind} "${ctx.resourceName}" 變成可被外部呼叫。`));
if (ctx.destination) {
console.log(chalk.gray(` 去向:${ctx.destination}`));
}
if (ctx.dataSummary) {
console.log(chalk.gray(` 涉及資料:${ctx.dataSummary}`));
} else {
console.log(chalk.gray(` 涉及資料:無法自動判斷,請自行確認此資源是否含敏感資料。`));
}
console.log(chalk.gray(` 任何能呼叫它的人都能取得它的輸出/能力。`));
console.log('');
console.log(chalk.cyan(` arcrun 可幫你保護它:要求呼叫者帶 API Key/設權限/限流(一個動作就能加)。`));
console.log(chalk.gray(` 若這是要公開的資料(如公開 API),可直接確認。`));
console.log('');
}