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
+109
View File
@@ -0,0 +1,109 @@
/**
* acr creds push [credentials.yaml]
*
* 讀取 credentials.yaml,以 ENCRYPTION_KEY 加密後 POST 至 cypher.arcrun.dev/credentials。
* Server 以 {api_key}:cred:{name} 為 KV key 存入 CREDENTIALS_KV(多租戶隔離)。
*
* 不再需要用戶提供 CF API Token 或 KV Namespace ID。
*/
import { readFileSync } from 'node:fs';
import yaml from 'js-yaml';
import chalk from 'chalk';
import ora from 'ora';
import { loadConfig, getCypherExecutorUrl } from '../lib/config.js';
async function encryptValue(value: string, encryptionKey: string): Promise<{ encrypted: string; iv: string }> {
const keyBytes = hexToUint8Array(encryptionKey);
const cryptoKey = await crypto.subtle.importKey(
'raw',
keyBytes.buffer as ArrayBuffer,
{ name: 'AES-GCM' },
false,
['encrypt'],
);
const ivBytes = crypto.getRandomValues(new Uint8Array(12));
const encoded = new TextEncoder().encode(value);
const cipherBuffer = await crypto.subtle.encrypt({ name: 'AES-GCM', iv: ivBytes }, cryptoKey, encoded);
return {
encrypted: Buffer.from(new Uint8Array(cipherBuffer)).toString('base64'),
iv: Buffer.from(ivBytes).toString('base64'),
};
}
function hexToUint8Array(hex: string): Uint8Array {
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < hex.length; i += 2) bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
return bytes;
}
export async function cmdCredsPush(filePath: string): Promise<void> {
const config = loadConfig();
if (config.mode === 'local') {
console.error(chalk.red('Local 模式不支援 acr creds push。'));
console.log(chalk.gray('請先執行 acr init 設定 Standard 模式,取得 API Key。'));
process.exit(1);
}
if (!config.api_key) {
console.error(chalk.red('缺少 api_key,請重新執行 acr init。'));
process.exit(1);
}
// 讀取 credentials.yaml
let creds: Record<string, string>;
try {
const raw = readFileSync(filePath, 'utf8');
creds = yaml.load(raw) as Record<string, string>;
} catch (e) {
console.error(chalk.red(`無法讀取 ${filePath}${e instanceof Error ? e.message : e}`));
process.exit(1);
}
const entries = Object.entries(creds).filter(([, v]) => typeof v === 'string' && v.length > 0);
if (entries.length === 0) {
console.log(chalk.yellow('credentials.yaml 中沒有有效的 credential(請取消注解並填入值)'));
return;
}
// 加密金鑰:優先從 config 讀(acr init 時自動寫入),其次從環境變數
const encryptionKey = config.encryption_key ?? process.env.ARCRUN_ENCRYPTION_KEY ?? '';
if (!encryptionKey || encryptionKey.length < 64) {
console.error(chalk.red(
'缺少 encryption_key。請重新執行 acr init 取得設定。',
));
process.exit(1);
}
const baseUrl = getCypherExecutorUrl(config);
console.log(chalk.bold(`\n 上傳 ${entries.length} 個 credentials 至 ${baseUrl}\n`));
for (const [name, value] of entries) {
const spinner = ora(` ${name}`).start();
try {
const { encrypted, iv } = await encryptValue(String(value), encryptionKey);
const res = await fetch(`${baseUrl}/credentials`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Arcrun-API-Key': config.api_key!,
},
body: JSON.stringify({ name, encrypted, iv }),
});
if (!res.ok) {
const errBody = await res.text().catch(() => '');
throw new Error(`HTTP ${res.status}: ${errBody.slice(0, 200)}`);
}
spinner.succeed(chalk.green(`${name}`));
} catch (e) {
spinner.fail(chalk.red(`${name} 失敗:${e instanceof Error ? e.message : e}`));
}
}
console.log(chalk.gray('\n Credential 已加密儲存。執行 workflow 時會自動注入,無需在 --input 手動帶 token。\n'));
}