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
+203
View File
@@ -0,0 +1,203 @@
/**
* Executions routes — LI SDD M2.1
*
* 對應 .agents/specs/llm-interface/ Milestone 2.1。給 AI 看 workflow 執行狀態的端點。
*
* - GET /executions/paused — 列當前所有 paused 的 workflow(等 callback resume
* - GET /executions/:task_id — 看單一 paused state 細節(含 trace、graph、node id
* - GET /workflows/:name/executions — 列某 workflow 最近 N 次執行 verdict
*
* 設計:純讀,無 side effect。所有路由要 api_key auth(防偷看他人 workflow state)。
*/
import { Hono } from 'hono';
import type { Bindings } from '../types';
import { listPausedRunsByApiKey } from '../lib/paused-runs';
export const executionsRouter = new Hono<{ Bindings: Bindings }>();
/**
* GET /executions/paused — 列當前 api_key 下所有 paused workflow
*
* 走 per-user index `paused_idx:{api_key}`(單 KV get,強 consistent,無 KV list 延遲)
* 取代舊的 `paused_run:*` prefix scanCF KV list 30-60 秒 eventual consistent
*/
executionsRouter.get('/executions/paused', async (c) => {
const apiKey = c.req.header('X-Arcrun-API-Key');
if (!apiKey) {
return c.json({
ok: false,
error_code: 'auth_missing',
human_message: '缺 X-Arcrun-API-Key header',
next_actions: ['call /me 取得你的 ak_xxx,加進 header'],
}, 401);
}
const limitParam = c.req.query('limit');
const limit = Math.min(Math.max(parseInt(limitParam || '20', 10), 1), 100);
const paused = await listPausedRunsByApiKey(c.env.EXEC_CONTEXT, apiKey, limit);
return c.json({
ok: true,
data: { count: paused.length, paused },
hints: paused.length > 0
? [`${paused.length} 個 workflow 等 callback resume。call get_execution_trace(task_id) 看細節`]
: ['沒有任何 paused workflow'],
});
});
/**
* GET /executions/:task_id — 看單一 paused workflow 的 statetrace、graph、context
*
* task_id 來源:trigger workflow 時 response 含 paused 結果,task_id 在 error 字串裡,
* 或前端 list_paused_executions 回的 task_id。
*
* 隔離:只能讀自己 api_key 的 state。
*/
executionsRouter.get('/executions/:task_id', async (c) => {
const apiKey = c.req.header('X-Arcrun-API-Key');
if (!apiKey) {
return c.json({
ok: false,
error_code: 'auth_missing',
human_message: '缺 X-Arcrun-API-Key header',
next_actions: ['加 X-Arcrun-API-Key header'],
}, 401);
}
const taskId = c.req.param('task_id');
const raw = await c.env.EXEC_CONTEXT.get(`paused_run:${taskId}`);
if (!raw) {
return c.json({
ok: false,
error_code: 'not_found',
human_message: `task_id "${taskId}" 沒對應的 paused state(可能已 resume 完、過 24h TTL 被 GC、或從未存在)`,
next_actions: [
'call /executions/paused 看當前所有 paused,確認 task_id 正確',
'若該 workflow 不是 paused 型,看 /workflows/:name/executions 查歷史 verdict',
],
}, 404);
}
let state: {
run_id: string;
graph?: unknown;
paused_node_id: string;
paused_context?: Record<string, unknown>;
paused_pending_result?: Record<string, unknown>;
trace_so_far?: unknown;
api_key?: string;
expires_at?: number;
};
try {
state = JSON.parse(raw);
} catch {
return c.json({
ok: false,
error_code: 'internal_error',
human_message: 'paused state JSON 損毀',
next_actions: ['告訴 leo / 平台維護者'],
}, 500);
}
if (state.api_key !== apiKey) {
return c.json({
ok: false,
error_code: 'not_found', // 不洩漏存在性
human_message: `task_id "${taskId}" 找不到`,
next_actions: ['確認 task_id 屬於你 (用 /executions/paused 列出)'],
}, 404);
}
return c.json({
ok: true,
data: {
task_id: taskId,
run_id: state.run_id,
paused_node_id: state.paused_node_id,
paused_context: state.paused_context,
paused_pending_result: state.paused_pending_result,
trace_so_far: state.trace_so_far,
expires_at: state.expires_at,
},
hints: [
'paused 狀態 = workflow 等 daemon callback。等對應 service 回 POST /workflows/resume 即可繼續',
'若 daemon 掛了,看 expires_at — 過 24h KV TTL 會 GC 此 state',
],
});
});
/**
* GET /workflows/:name/executions — 看某 workflow 最近 N 次執行 verdict
*
* 走 ANALYTICS_KV `stats:{workflowId}:*` prefix scan。
*
* workflowId 等於 webhook nameexecution-logger 寫入時用 graph.id ?? name)。
*
* 限制:ANALYTICS_KV list 沒辦法依 timestamp 排序,只能拿 key 後段 timestamp 排。
*/
executionsRouter.get('/workflows/:name/executions', async (c) => {
const apiKey = c.req.header('X-Arcrun-API-Key');
if (!apiKey) {
return c.json({
ok: false,
error_code: 'auth_missing',
human_message: '缺 X-Arcrun-API-Key header',
next_actions: ['加 X-Arcrun-API-Key header'],
}, 401);
}
const name = c.req.param('name');
const limitParam = c.req.query('limit');
const limit = Math.min(Math.max(parseInt(limitParam || '10', 10), 1), 100);
// 確認 workflow 是該 api_key 的(防偷看他人)
const wfRaw = await c.env.WEBHOOKS.get(`${apiKey}:wf:${name}`, 'text');
if (!wfRaw) {
return c.json({
ok: false,
error_code: 'not_found',
human_message: `workflow "${name}" 不存在或不屬於你`,
next_actions: ['call /webhooks/named 看你有什麼 workflow'],
}, 404);
}
// 撈 stats:{name}:* 全 list(每個 key 含 timestamp 後綴)
const list = await c.env.ANALYTICS_KV.list({ prefix: `stats:${name}:`, limit: 1000 });
// 按 timestamp 降序(key suffix 是 unix ms
const sorted = [...list.keys].sort((a, b) => {
const ta = parseInt(a.name.split(':').pop() ?? '0', 10);
const tb = parseInt(b.name.split(':').pop() ?? '0', 10);
return tb - ta;
}).slice(0, limit);
const executions = [];
for (const key of sorted) {
const raw = await c.env.ANALYTICS_KV.get(key.name);
if (!raw) continue;
try {
const record = JSON.parse(raw);
executions.push({
timestamp: key.name.split(':').pop(),
...record,
});
} catch {
// skip
}
}
return c.json({
ok: true,
data: {
workflow_name: name,
count: executions.length,
executions,
},
hints: executions.length === 0
? ['尚未有任何執行紀錄(或都過了 90d TTL)。先 call /webhooks/named/:name/trigger 跑一次']
: [`最近 ${executions.length} 次。看到 verdict=failed 的,call /executions/:task_id 看 paused state 或繼續 debug`],
});
});