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:
@@ -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'));
|
||||
}
|
||||
Reference in New Issue
Block a user