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
+145
View File
@@ -0,0 +1,145 @@
/**
* acr init — 互動式初始化設定
* 詢問 CF Account ID、KV namespace、API Token、email
* 呼叫 arcrun.dev 取得 API Key,寫入 ~/.arcrun/config.yaml
*/
import { createInterface } from 'node:readline/promises';
import { writeFileSync, existsSync } from 'node:fs';
import { join } from 'node:path';
import chalk from 'chalk';
import { saveConfig, type ArcrunConfig } from '../lib/config.js';
const ARCRUN_REGISTER_URL = 'https://api.arcrun.dev/register';
async function prompt(rl: ReturnType<typeof createInterface>, question: string): Promise<string> {
const answer = await rl.question(chalk.cyan(`? ${question}: `));
return answer.trim();
}
export async function cmdInit(options: { selfHosted?: boolean }): Promise<void> {
const rl = createInterface({ input: process.stdin, output: process.stdout });
console.log(chalk.bold('\n arcrun 初始化設定\n'));
try {
if (options.selfHosted) {
await initSelfHosted(rl);
} else {
await initStandard(rl);
}
} finally {
rl.close();
}
}
async function initStandard(rl: ReturnType<typeof createInterface>): Promise<void> {
console.log(chalk.gray(' Standard 模式:使用 arcrun.dev 的執行引擎,credential 存在你自己的 CF KV\n'));
const accountId = await prompt(rl, '你的 Cloudflare Account ID');
const kvNamespaceId = await prompt(rl,
'USER_KV Namespace ID(先至 CF Dashboard 建立一個 KV 後貼上)'
);
const cfApiToken = await prompt(rl,
'CF API Token(只需 KV Edit 權限,供 acr 讀寫你的 KV'
);
const email = await prompt(rl, 'Email(取得 arcrun.dev API Key');
process.stdout.write(chalk.gray('\n → 向 arcrun.dev 取得 API Key...'));
let apiKey: string;
try {
const res = await fetch(ARCRUN_REGISTER_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, cf_api_token: cfApiToken }),
});
if (!res.ok) {
const err = await res.text();
throw new Error(`API Key 取得失敗(${res.status}):${err}`);
}
const data = await res.json() as { api_key: string; tenant_id: string };
apiKey = data.api_key;
console.log(chalk.green(' ✓'));
} catch (e) {
console.log(chalk.yellow(' ✗(離線模式,請稍後執行 acr init 重試)'));
apiKey = '';
}
const config: ArcrunConfig = {
mode: 'standard',
cloudflare_account_id: accountId,
user_kv_namespace_id: kvNamespaceId,
cf_api_token: cfApiToken,
api_key: apiKey,
};
saveConfig(config);
// 建立空白 credentials.yaml
createCredentialsYamlIfMissing();
console.log(chalk.green('\n ✓ 設定完成 → ~/.arcrun/config.yaml'));
if (apiKey) {
console.log(chalk.green(` ✓ API Key${apiKey.slice(0, 8)}...(已安全儲存)`));
}
console.log(chalk.green(' ✓ 建立 credentials.yaml(已加入 .gitignore\n'));
console.log(chalk.gray(' 你的 credential 與 workflow 存在你自己的 CF KVarcrun 不會儲存它們。\n'));
console.log(' 下一步:');
console.log(chalk.cyan(' acr creds push credentials.yaml') + ' # 上傳加密 credentials');
console.log(chalk.cyan(' acr push workflow.yaml') + ' # 部署 workflow');
console.log(chalk.cyan(' acr run <workflow_name>') + ' # 執行 workflow\n');
}
async function initSelfHosted(rl: ReturnType<typeof createInterface>): Promise<void> {
console.log(chalk.gray(' Self-hosted 模式:自行部署所有 Worker 到你的 Cloudflare 帳號\n'));
const accountId = await prompt(rl, '你的 Cloudflare Account ID');
const cypherUrl = await prompt(rl, 'Cypher Executor URL(部署後的 workers.dev URL');
const webhooksKvId = await prompt(rl, 'WEBHOOKS KV Namespace ID');
const credentialsKvId = await prompt(rl, 'CREDENTIALS_KV Namespace ID');
const wasmBucket = await prompt(rl, 'WASM_BUCKET 名稱');
const cfApiToken = await prompt(rl, 'CF API TokenKV Edit 權限)');
const config: ArcrunConfig = {
mode: 'self-hosted',
cloudflare_account_id: accountId,
cypher_executor_url: cypherUrl,
webhooks_kv_namespace_id: webhooksKvId,
credentials_kv_namespace_id: credentialsKvId,
wasm_bucket: wasmBucket,
cf_api_token: cfApiToken,
multi_tenant: false,
};
saveConfig(config);
createCredentialsYamlIfMissing();
console.log(chalk.green('\n ✓ 設定完成 → ~/.arcrun/config.yaml'));
console.log(chalk.green(' ✓ 建立 credentials.yaml\n'));
}
function createCredentialsYamlIfMissing(): void {
const credPath = join(process.cwd(), 'credentials.yaml');
if (!existsSync(credPath)) {
writeFileSync(credPath,
'# arcrun credentials — 不要提交至 git\n' +
'# 執行 acr creds push 上傳加密後的 credential 到你的 CF KV\n\n' +
'# gmail_token: "your-google-oauth-token"\n' +
'# telegram_bot_token: "your-telegram-bot-token"\n' +
'# google_oauth: "your-google-oauth-token"\n' +
'# line_token: "your-line-notify-token"\n',
'utf8'
);
}
// 確保 .gitignore 排除 credentials.yaml
const gitignorePath = join(process.cwd(), '.gitignore');
if (existsSync(gitignorePath)) {
const content = require('node:fs').readFileSync(gitignorePath, 'utf8');
if (!content.includes('credentials.yaml')) {
require('node:fs').appendFileSync(gitignorePath, '\ncredentials.yaml\n');
}
}
}