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
+145
View File
@@ -0,0 +1,145 @@
/**
* acr push <workflow.yaml>
*
* 解析 workflow.yaml,透過 /cypher/search 取得執行圖,
* 然後 POST 至 cypher.arcrun.dev/webhooks/named(帶 X-Arcrun-API-Key)。
* Server 以 {api_key}:wf:{name} 為 KV key 存入 WEBHOOKS KV。
*
* 不再需要用戶的 CF API Token 或 KV Namespace ID。
*/
import chalk from 'chalk';
import ora from 'ora';
import { loadConfig, getCypherExecutorUrl } from '../lib/config.js';
import { loadWorkflowYaml, parseTriplets, validateRelations } from '../lib/yaml-parser.js';
import { obtainExposureConsent } from '../lib/exposure-warning.js';
export async function cmdPush(filePath: string): Promise<void> {
const config = loadConfig();
if (config.mode === 'local') {
console.error(chalk.red('Local 模式不支援 acr pushWebhook 部署需要 Standard 模式)。'));
console.log(chalk.gray('請執行 acr init 取得 API Key,或直接用 acr run 本機測試。'));
process.exit(1);
}
if (!config.api_key) {
console.error(chalk.red('缺少 api_key,請重新執行 acr init。'));
process.exit(1);
}
// 解析 YAML
const spinner = ora('解析 workflow.yaml').start();
let workflow;
try {
workflow = loadWorkflowYaml(filePath);
const triplets = parseTriplets(workflow.flow);
validateRelations(triplets);
spinner.succeed(`解析完成:${workflow.name}${triplets.length} 條三元組)`);
} catch (e) {
spinner.fail(chalk.red(`解析失敗:${e instanceof Error ? e.message : e}`));
process.exit(1);
}
const executorUrl = getCypherExecutorUrl(config);
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'X-Arcrun-API-Key': config.api_key,
};
// 向 /cypher/search 取得執行圖
const searchSpinner = ora('取得執行圖').start();
let graph: unknown;
try {
const res = await fetch(`${executorUrl}/cypher/search`, {
method: 'POST',
headers,
body: JSON.stringify({ triplets: workflow.flow }),
});
if (!res.ok) {
const err = await res.text();
searchSpinner.fail(chalk.red(`執行圖解析失敗(${res.status}):${err.slice(0, 200)}`));
process.exit(1);
}
const data = await res.json() as { cypher: { nodes: unknown[]; edges: unknown[] }; missing: string[] };
if (data.missing?.length > 0) {
searchSpinner.fail(chalk.red(`以下零件不存在:${data.missing.join(', ')}\n執行 acr parts 查看可用零件。`));
process.exit(1);
}
// 附上 id / name,並將 workflow.config 套入節點(componentId + data
const rawGraph = data.cypher as { nodes: Array<{ id: string; componentId?: string; data?: Record<string, unknown> }>; edges: unknown[] };
const cfg = (workflow.config ?? {}) as Record<string, Record<string, unknown>>;
const nodes = rawGraph.nodes.map(node => {
const nodeCfg = cfg[node.id];
if (!nodeCfg) return node;
const { component, ...params } = nodeCfg;
return {
...node,
componentId: typeof component === 'string' ? component : node.componentId,
data: Object.keys(params).length > 0 ? { ...(node.data ?? {}), ...params } : node.data,
};
});
graph = { id: workflow.name, name: workflow.name, nodes, edges: rawGraph.edges };
searchSpinner.succeed('執行圖解析完成');
} catch (e) {
searchSpinner.fail(chalk.red(`網路錯誤:${e instanceof Error ? e.message : e}`));
process.exit(1);
}
// 資料外流警示:部署 webhook = 把 workflow 變對外可呼叫 endpoint(暴露面)。
// 已同意過(本機記住)→ 回非 null 自動放行;未同意/取消/非互動 → null → 中止。
// server 端獨立存法律憑證並強制(防 CLI 被繞過)。
const consent = await obtainExposureConsent({
kind: 'workflow',
resourceName: workflow.name,
destination: `${executorUrl}/webhooks/named/${workflow.name}/trigger`,
});
if (!consent) {
process.exit(1);
}
// POST 至 /webhooks/named
const deploySpinner = ora(`部署 "${workflow.name}" 至 ${executorUrl}`).start();
try {
const res = await fetch(`${executorUrl}/webhooks/named`, {
method: 'POST',
headers,
body: JSON.stringify({
name: workflow.name,
graph,
config: workflow.config ?? {},
description: workflow.description ?? '',
exposure_consent: consent ?? undefined,
}),
});
if (!res.ok) {
const err = await res.text();
deploySpinner.fail(chalk.red(`部署失敗(${res.status}):${err.slice(0, 200)}`));
process.exit(1);
}
const data = await res.json() as { name: string; webhook_url: string; created_at: string };
deploySpinner.succeed(chalk.green(`✓ "${workflow.name}" 已部署`));
console.log(chalk.bold(`\n Webhook URL${chalk.cyan(data.webhook_url)}`));
console.log(chalk.gray(` 需帶 HeaderX-Arcrun-API-Key: ${config.api_key.slice(0, 8)}...`));
console.log('');
console.log(chalk.gray(' 測試執行:'));
console.log(` ${chalk.cyan(`acr run ${workflow.name}`)}`);
console.log('');
console.log(chalk.gray(' curl 觸發範例:'));
console.log(` ${chalk.cyan(`curl -X POST ${data.webhook_url} \\`)}`);
console.log(` ${chalk.cyan(` -H 'X-Arcrun-API-Key: ${config.api_key}' \\`)}`);
console.log(` ${chalk.cyan(` -H 'Content-Type: application/json' \\`)}`);
console.log(` ${chalk.cyan(` -d '{"key": "value"}'`)}`);
console.log('');
} catch (e) {
deploySpinner.fail(chalk.red(`部署失敗:${e instanceof Error ? e.message : e}`));
process.exit(1);
}
}