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
@@ -0,0 +1,108 @@
/**
* arcrun platform_crypto Worker
*
* POST / → JSON input {action, ...} → JSON output
*
* Actions:
* generate_api_key — HMAC-SHA256(email, ENCRYPTION_KEY) → ak_{hex[:32]}
* encrypt — AES-GCM(plaintext, ENCRYPTION_KEY) → {encrypted, iv}base64
* random_token — crypto random bytes → hex string
*
* 安全邊界:ENCRYPTION_KEY 只存在於 closure,永不進入外部(rule 02 §2.2)。
* 此 Worker 直接用 crypto.subtle 實作,不走 WASM runner。
* TinyGo WASM async host function 在 Cloudflare Workers 的 u6u namespace 不支援 Promise.
* WASM 零件 (registry/components/platform_crypto/) 保留作為 edge-Go 移植時的參考。
*/
import { Hono } from 'hono';
import { cors } from 'hono/cors';
type Env = {
ENCRYPTION_KEY: string;
};
type Input = {
action: string;
email?: string;
plaintext?: string;
bytes?: number;
};
const app = new Hono<{ Bindings: Env }>();
app.use('*', cors());
app.get('/', (c) => c.json({ ok: true, component: 'platform_crypto' }));
app.post('/', async (c) => {
let input: Input;
try {
input = await c.req.json() as Input;
} catch {
return c.json({ success: false, error: 'request body must be JSON' }, 400);
}
const encryptionKey = c.env.ENCRYPTION_KEY;
if (!encryptionKey) {
return c.json({ success: false, error: 'ENCRYPTION_KEY not configured' }, 503);
}
try {
switch (input.action) {
case 'generate_api_key': {
if (!input.email) return c.json({ success: false, error: 'email 必填' }, 400);
const apiKey = await generateApiKey(input.email, encryptionKey);
return c.json({ success: true, api_key: apiKey });
}
case 'encrypt': {
if (!input.plaintext) return c.json({ success: false, error: 'plaintext 必填' }, 400);
const { encrypted, iv } = await aesEncrypt(input.plaintext, encryptionKey);
return c.json({ success: true, encrypted, iv });
}
case 'random_token': {
const numBytes = (input.bytes ?? 32) > 0 ? (input.bytes ?? 32) : 32;
const token = randomHex(numBytes);
return c.json({ success: true, token });
}
default:
return c.json({ success: false, error: `不支援的 action: ${input.action}` }, 400);
}
} catch (e) {
return c.json(
{ success: false, error: e instanceof Error ? e.message : String(e) },
500,
);
}
});
export default app;
// ── Crypto implementations (rule 02 §2.2: crypto.subtle 只准在 wasi-shim.ts 或 platform_crypto) ──
async function generateApiKey(email: string, encryptionKey: string): Promise<string> {
const keyBytes = new TextEncoder().encode(encryptionKey.slice(0, 32));
const cryptoKey = await crypto.subtle.importKey(
'raw', keyBytes, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'],
);
const sig = await crypto.subtle.sign('HMAC', cryptoKey, new TextEncoder().encode(email));
const hex = Array.from(new Uint8Array(sig)).map(b => b.toString(16).padStart(2, '0')).join('');
return 'ak_' + hex.slice(0, 32);
}
async function aesEncrypt(plaintext: string, encryptionKey: string): Promise<{ encrypted: string; iv: string }> {
const keyBytes = new TextEncoder().encode(encryptionKey.slice(0, 32));
const cryptoKey = await crypto.subtle.importKey('raw', keyBytes, { name: 'AES-GCM' }, false, ['encrypt']);
const iv = crypto.getRandomValues(new Uint8Array(12));
const enc = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
cryptoKey,
new TextEncoder().encode(plaintext),
);
const toB64 = (buf: ArrayBuffer | Uint8Array) =>
btoa(String.fromCharCode(...new Uint8Array(buf instanceof ArrayBuffer ? buf : buf)));
return { encrypted: toB64(enc), iv: toB64(iv) };
}
function randomHex(numBytes: number): string {
const arr = crypto.getRandomValues(new Uint8Array(numBytes));
return Array.from(arr).map(b => b.toString(16).padStart(2, '0')).join('');
}