feat(arcrun): implement arcrun MVP — open-source AI workflow engine

Phase 1-5 complete per .agents/specs/u6u-core-mvp/:

**Phase 1 — Cherry-pick & cleanup**
- Create arcrun/ from cypher-executor, credentials, builtins, registry
- Remove 9 InkStone Service Bindings (KBDB, REGISTRY, CLINIC_*, AICEO, MINI_ME)
- Rewrite component-loader: 3-layer (builtin → WASM_BUCKET R2 → error)
- Remove autoPublishMissing.ts, proxy.ts (AICEO), execution-logger.ts (KBDB)
- Clean all KV namespace IDs and InkStone internal URLs from config files

**Phase 2 — contract.yaml completeness**
- Add credentials_required to gmail, google_sheets, telegram, line_notify
- Add config_example to all 21 components with annotated field descriptions

**Phase 3 — Credential injection**
- Add credential-injector.ts: AES-GCM decrypt from CREDENTIALS_KV
- Integrate into GraphExecutor before WASM execution
- Structured errors with repair instructions when credential missing

**Phase 4 — CLI (acr)**
- cli/package.json: arcrun package, bin: acr, deps: commander/js-yaml/chalk/ora
- 8 commands: init, creds push, push, run, validate, parts, list, logs
- Standard mode: writes directly to user's CF KV via CF REST API
- acr init: interactive setup with arcrun.dev API Key registration

**Phase 5 — Open source release prep**
- README.md: 5-minute quickstart, component table, workflow YAML syntax
- CONTRIBUTING.md: TinyGo dev env, component scaffolding, submission flow
- Security audit: no InkStone internal URLs/IDs in committed files
- .gitignore: exclude credentials.yaml, .wrangler, *.wasm

https://claude.ai/code/session_01BnCdSLVH8tUed9VrrPavgT
This commit is contained in:
Claude
2026-04-16 04:06:25 +00:00
commit 2707fca32b
155 changed files with 17413 additions and 0 deletions
@@ -0,0 +1,37 @@
// createCredential:儲存 credentialname + secret + type),加密後存入 KV
import type { Context } from 'hono';
import type { Bindings, CredentialRecord } from '../types';
import { encrypt } from './crypto';
function slugify(name: string): string {
return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || `cred-${Date.now()}`;
}
export async function handleCreateCredential(c: Context<{ Bindings: Bindings }>) {
const body = await c.req.json().catch(() => null);
if (!body) return c.json({ success: false, error: '無效的 JSON body' }, 400);
const { name, secret, type } = body;
if (!name) return c.json({ success: false, error: 'name 必填' }, 400);
if (!secret) return c.json({ success: false, error: 'secret 必填' }, 400);
if (!type) return c.json({ success: false, error: 'type 必填(例: api_key, bearer_token, google_oauth' }, 400);
// 使用 ENCRYPTION_KEY secret;若未設定,使用 fallback(開發環境)
const hexKey = c.env.ENCRYPTION_KEY || '0'.repeat(64);
const { encrypted, iv } = await encrypt(String(secret), hexKey);
const id = slugify(String(name));
const record: CredentialRecord = {
id,
name: String(name),
type: String(type),
encrypted_secret: encrypted,
iv,
created_at: Date.now(),
};
await c.env.CREDENTIALS_KV.put(`cred:${id}`, JSON.stringify(record));
return c.json({ success: true, data: { id, name: record.name, type: record.type } }, 201);
}
+28
View File
@@ -0,0 +1,28 @@
// cryptoAES-GCM 加解密工具(Web Crypto API
/** 從 hex 字串匯入 AES-GCM key */
async function importKey(hexKey: string): Promise<CryptoKey> {
const raw = new Uint8Array(hexKey.match(/.{1,2}/g)!.map(b => parseInt(b, 16)));
return crypto.subtle.importKey('raw', raw, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']);
}
/** 加密 plaintext,回傳 { encrypted, iv }(均為 base64 */
export async function encrypt(plaintext: string, hexKey: string): Promise<{ encrypted: string; iv: string }> {
const key = await importKey(hexKey);
const iv = crypto.getRandomValues(new Uint8Array(12));
const encoded = new TextEncoder().encode(plaintext);
const cipherBuf = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, encoded);
return {
encrypted: btoa(String.fromCharCode(...new Uint8Array(cipherBuf))),
iv: btoa(String.fromCharCode(...iv)),
};
}
/** 解密,回傳 plaintext */
export async function decrypt(encrypted: string, iv: string, hexKey: string): Promise<string> {
const key = await importKey(hexKey);
const ivBuf = Uint8Array.from(atob(iv), c => c.charCodeAt(0));
const cipherBuf = Uint8Array.from(atob(encrypted), c => c.charCodeAt(0));
const plainBuf = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: ivBuf }, key, cipherBuf);
return new TextDecoder().decode(plainBuf);
}
@@ -0,0 +1,16 @@
// deleteCredential:刪除指定 credentialby name or id
import type { Context } from 'hono';
import type { Bindings } from '../types';
export async function handleDeleteCredential(c: Context<{ Bindings: Bindings }>) {
const name = c.req.param('name');
const key = `cred:${name}`;
const existing = await c.env.CREDENTIALS_KV.get(key);
if (!existing) {
return c.json({ success: false, error: `找不到 credential: ${name}` }, 404);
}
await c.env.CREDENTIALS_KV.delete(key);
return c.json({ success: true, data: { deleted: name } });
}
@@ -0,0 +1,21 @@
// getCredentialSecret:解密並回傳 secret(內部使用,Cypher Executor inject 用)
// 此端點只接受內部呼叫(需 Authorization: Bearer <INTERNAL_TOKEN>
import type { Context } from 'hono';
import type { Bindings, CredentialRecord } from '../types';
import { decrypt } from './crypto';
export async function handleGetSecret(c: Context<{ Bindings: Bindings }>) {
const name = c.req.param('name');
const raw = await c.env.CREDENTIALS_KV.get(`cred:${name}`);
if (!raw) return c.json({ success: false, error: `找不到 credential: ${name}` }, 404);
const record = JSON.parse(raw) as CredentialRecord;
const hexKey = c.env.ENCRYPTION_KEY || '0'.repeat(64);
try {
const secret = await decrypt(record.encrypted_secret, record.iv, hexKey);
return c.json({ success: true, data: { id: record.id, type: record.type, secret } });
} catch {
return c.json({ success: false, error: '解密失敗,請確認 ENCRYPTION_KEY 是否正確' }, 500);
}
}
@@ -0,0 +1,18 @@
// listCredentials:列出所有 credential(只回傳 id/name/type,不含 secret
import type { Context } from 'hono';
import type { Bindings, CredentialRecord, CredentialSummary } from '../types';
export async function handleListCredentials(c: Context<{ Bindings: Bindings }>) {
const { keys } = await c.env.CREDENTIALS_KV.list({ prefix: 'cred:' });
const summaries: CredentialSummary[] = [];
for (const key of keys) {
const raw = await c.env.CREDENTIALS_KV.get(key.name);
if (!raw) continue;
const record = JSON.parse(raw) as CredentialRecord;
summaries.push({ id: record.id, name: record.name, type: record.type, created_at: record.created_at });
}
summaries.sort((a, b) => b.created_at - a.created_at);
return c.json({ success: true, data: { credentials: summaries, count: summaries.length } });
}
+26
View File
@@ -0,0 +1,26 @@
// u6u-credentials Worker — Credential 儲存與注入
// index.ts 只做路由宣告,業務邏輯在 actions/INV Layer 1
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import type { Bindings } from './types';
import { handleCreateCredential } from './actions/createCredential';
import { handleListCredentials } from './actions/listCredentials';
import { handleDeleteCredential } from './actions/deleteCredential';
import { handleGetSecret } from './actions/getCredentialSecret';
const app = new Hono<{ Bindings: Bindings }>();
app.use('*', cors());
// Health check
app.get('/', c => c.json({ service: 'u6u-credentials', version: '1.0.0', status: 'ok' }));
// POST /credentials — 建立 credential(加密存入 KV
// GET /credentials — 列出所有 credential(不含 secret
// DELETE /credentials/:name — 刪除 credential
// GET /credentials/:name/secret — 取得解密 secretCypher Executor inject 用)
app.post ('/credentials', handleCreateCredential);
app.get ('/credentials', handleListCredentials);
app.delete('/credentials/:name', handleDeleteCredential);
app.get ('/credentials/:name/secret', handleGetSecret);
export default app;
+24
View File
@@ -0,0 +1,24 @@
// u6u-credentials Worker 型別定義
export type Bindings = {
CREDENTIALS_KV: KVNamespace;
ENCRYPTION_KEY: string; // hex-encoded 256-bit AES keywrangler secret
ENVIRONMENT: string;
};
export interface CredentialRecord {
id: string; // 用 name slugify 生成
name: string; // 用戶命名(human-readable
type: string; // api_key / bearer_token / google_oauth / telegram_bot_token / ...
encrypted_secret: string; // AES-GCM base64 encrypted
iv: string; // base64 IV
created_at: number;
}
// 對外回傳(不含 secret
export interface CredentialSummary {
id: string;
name: string;
type: string;
created_at: number;
}