diff --git a/cli/src/commands/push.ts b/cli/src/commands/push.ts index a29689c..9fa6e77 100644 --- a/cli/src/commands/push.ts +++ b/cli/src/commands/push.ts @@ -11,7 +11,6 @@ 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 { const config = loadConfig(); @@ -96,17 +95,8 @@ export async function cmdPush(filePath: string): Promise { 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); - } + // 暴露 consent 閘已移除(leo 2026-06-29,Arcrun#13):arcrun 是給 AI 用的系統, + // push/暴露不再需要人類確認,AI/MCP 隨時可部署。暴露風險由用戶自負(同 n8n 建 webhook)。 // POST 至 /webhooks/named const deploySpinner = ora(`部署 "${workflow.name}" 至 ${executorUrl}`).start(); @@ -119,7 +109,6 @@ export async function cmdPush(filePath: string): Promise { graph, config: workflow.config ?? {}, description: workflow.description ?? '', - exposure_consent: consent ?? undefined, }), }); diff --git a/cli/src/commands/recipe.ts b/cli/src/commands/recipe.ts index 793f712..3aac348 100644 --- a/cli/src/commands/recipe.ts +++ b/cli/src/commands/recipe.ts @@ -7,7 +7,6 @@ import chalk from 'chalk'; import ora from 'ora'; import { readFileSync, existsSync } from 'node:fs'; import { loadConfig, getCypherExecutorUrl, DEFAULT_PUBLIC_LIBRARY_URL } from '../lib/config.js'; -import { obtainExposureConsent } from '../lib/exposure-warning.js'; import yaml from 'js-yaml'; interface RecipeYaml { @@ -70,16 +69,7 @@ export async function cmdRecipePush(filePath: string): Promise { const executorUrl = getCypherExecutorUrl(config); - // 資料外流警示:recipe 定義一個資料去向(endpoint)。首次 push 需人類明示同意(公私一視同仁)。 - // 已同意過(本機記住)→ 回非 null 自動放行;未同意/取消/非互動 → null → 中止。 - const consent = await obtainExposureConsent({ - kind: 'recipe', - resourceName: recipe.canonical_id, - destination: recipe.endpoint, - }); - if (!consent) { - process.exit(1); - } + // 暴露 consent 閘已移除(leo 2026-06-29,Arcrun#13):recipe push 不再需要人類確認。 const spinner = ora(`上傳 recipe "${recipe.canonical_id}"`).start(); @@ -90,7 +80,7 @@ export async function cmdRecipePush(filePath: string): Promise { 'Content-Type': 'application/json', 'X-Arcrun-API-Key': config.api_key, }, - body: JSON.stringify({ ...recipe, exposure_consent: consent ?? undefined }), + body: JSON.stringify(recipe), }); const data = await res.json() as { success: boolean; recipe?: RecipeDefinition; error?: string }; @@ -307,7 +297,7 @@ export async function cmdRecipePull(canonicalId: string, author?: string): Promi return; } - // 2. 寫進自己私庫(POST /recipes,帶 derived_from 溯源 + 種子級同意:pull 公庫公共資料非新暴露)。 + // 2. 寫進自己私庫(POST /recipes,帶 derived_from 溯源)。 const r = pub.recipe; const executorUrl = getCypherExecutorUrl(config); const installRes = await fetch(`${executorUrl}/recipes`, { @@ -316,11 +306,6 @@ export async function cmdRecipePull(canonicalId: string, author?: string): Promi body: JSON.stringify({ ...r, derived_from: r.uuid, // 溯源:私庫這份來自公庫哪個 uuid - exposure_consent: { - confirmed_by_human: true, - understood: `pull from public library: ${canonicalId}`, - confirmed_at: new Date().toISOString(), - }, }), }); const inst = await installRes.json() as { success: boolean; recipe?: RecipeDefinition; error?: string }; @@ -355,13 +340,7 @@ export async function cmdRecipeSubmitP(canonicalId: string, author?: string): Pr process.exit(1); } - // 2. 投稿到公庫 = 暴露面 → 取得人類明示同意(mindset §6)。 - const consent = await obtainExposureConsent({ - kind: 'recipe', - resourceName: canonicalId, - destination: `公庫(${DEFAULT_PUBLIC_LIBRARY_URL})`, - }); - if (!consent) process.exit(1); + // 暴露 consent 閘已移除(leo 2026-06-29,Arcrun#13):投稿公庫不再需要人類確認。 const spinner = ora(`投稿 recipe「${canonicalId}」到公庫`).start(); try { @@ -373,7 +352,6 @@ export async function cmdRecipeSubmitP(canonicalId: string, author?: string): Pr author: author ?? my.recipe.author, derived_from: my.recipe.derived_from ?? my.recipe.uuid, submitter: author ?? config.api_key, - exposure_consent: consent, }), }); const data = await res.json() as { success: boolean; recipe?: { uuid?: string; author?: string }; error?: string }; diff --git a/cli/src/lib/config.ts b/cli/src/lib/config.ts index 3ee60c8..7c7a5f7 100644 --- a/cli/src/lib/config.ts +++ b/cli/src/lib/config.ts @@ -34,9 +34,8 @@ export interface ArcrunConfig { // 未設/false → base 維持 LIKE keyword(free-tier 友善,不建 index、不花費)。 // 開法:設 kbdb_embed:true → redeploy(acr update)。「CC 幫開」=CC 寫此欄 true + 跑 acr update。 kbdb_embed?: boolean; - // 資料外流警示:本機記住「已同意暴露 / 選擇不再警示」的資源,避免每次 push 重問(§3 首次問記住)。 - // key 格式:`{kind}:{resourceName}`(如 "webhook:contacts_lookup" / "recipe:kbdb_get")。 - // 注意:這只是 CLI 端 UX(不重問);server 端獨立存法律憑證並強制(防 CLI 被繞過)。 + // 暴露 consent 閘已移除(leo 2026-06-29,Arcrun#13)。此欄位保留只為向後相容舊 config.yaml + // (讀到不報錯,不再寫入/檢查)。 exposure_consented?: Record; } diff --git a/cli/src/lib/exposure-warning.ts b/cli/src/lib/exposure-warning.ts deleted file mode 100644 index 6a0527c..0000000 --- a/cli/src/lib/exposure-warning.ts +++ /dev/null @@ -1,127 +0,0 @@ -/** - * 資料外流警示 — CLI 互動(data-exfil-warning SDD §1a / B) - * - * 觸發策略:只在「資料變成可被外部呼叫」時警示(webhook 部署 / recipe push)。 - * 互動形式(richblack):仿 GCP 刪 project —— 要用戶打資源名證明讀了警示(比 y/n 硬,不用打一大串)。 - * 同意 = 法律憑證:回傳的 ExposureConsent 帶 understood(用戶打的內容)+ 時間,server 端 log。 - * 誠實限制:非 TTY(AI 直跑)無 --confirm-exposure → 拒絕(AI 不該替人類確認暴露)。 - */ -import { createInterface } from 'node:readline/promises'; -import chalk from 'chalk'; -import { loadConfig, saveConfig } from './config.js'; - -export interface ExposureConsent { - confirmed_by_human: true; - understood: string; - confirmed_at: string; - suppress_future?: boolean; -} - -// 註(2026-05-30 信任修正):移除 --confirm-exposure / --suppress-warning 旗標。 -// 理由:arcrun 是 AI 的工具,AI 自己能加旗標 = 自己批准自己 = 閘門虛設(違 DECISIONS §7)。 -// 唯一通過 = 人類在 TTY 互動輸入資源名(AI 非互動環境生不出)。「以後不再問」改成互動中詢問。 -export interface ExposureWarningOptions { - // 預留:未來 CI 用「人類預先簽的 token」(非 AI 能生的 flag)。第一期不做。 - _reserved?: never; -} - -export interface ExposureContext { - /** 動作種類,顯示用:'webhook' | 'recipe' */ - kind: string; - /** 資源名(用戶要打這個字確認)*/ - resourceName: string; - /** 暴露後的 URL / 去向(顯示用,可選) */ - destination?: string; - /** 這個資源讀取/送出什麼(盡力盤,盤不出傳 undefined) */ - dataSummary?: string; -} - -/** - * 取得暴露同意。回傳 ExposureConsent(放進 push 請求 body)。 - * 未取得同意 → 印訊息並 return null(呼叫端應中止)。 - */ -export async function obtainExposureConsent( - ctx: ExposureContext, - opts: ExposureWarningOptions = {}, -): Promise { - const nowIso = new Date().toISOString(); - const memKey = `${ctx.kind}:${ctx.resourceName}`; - - // §3 首次問記住:本機已記錄同意此資源 → 不重問(server 端仍存法律憑證並強制)。 - const cfg = loadConfig(); - const prior = cfg.exposure_consented?.[memKey]; - if (prior) { - return { - confirmed_by_human: true, - understood: `先前已同意暴露 ${ctx.resourceName}(${prior.confirmed_at}${prior.suppress_future ? ',已選不再警示' : ''})`, - confirmed_at: prior.confirmed_at, - suppress_future: prior.suppress_future, - }; - } - - // 非 TTY(AI 直跑)→ 一律拒絕,無捷徑。AI 不該、也不能替人類確認暴露。 - // (移除了 --confirm-exposure 旗標:那是 AI 自己能加的後門,等於自己批准自己。) - if (!process.stdin.isTTY) { - console.error(chalk.red('\n⚠️ 此動作會把資源變成可被外部呼叫(暴露/送出資料),需人類明示同意。')); - console.error(chalk.gray(' 你(AI)無法確認暴露——這必須由人類在終端機親自執行、輸入資源名確認。')); - console.error(chalk.gray(' 請把這件事交給人類做。\n')); - return null; - } - - // 互動式警示 + 打資源名確認(唯一通過路徑,AI 生不出這個輸入) - printWarning(ctx); - const rl = createInterface({ input: process.stdin, output: process.stdout }); - try { - const answer = (await rl.question( - chalk.bold(` 確認暴露?請輸入資源名 "${ctx.resourceName}" 以繼續(或 Ctrl-C 取消):`), - )).trim(); - if (answer !== ctx.resourceName) { - console.error(chalk.red(`\n 輸入不符(需輸入 "${ctx.resourceName}")。已取消,未暴露。\n`)); - return null; - } - // 互動中詢問「以後不再問」(人類選,不是 AI 加旗標) - const suppressAns = (await rl.question( - chalk.gray(` 以後此資源(${ctx.resourceName})的暴露不再提醒?(y/N):`), - )).trim().toLowerCase(); - const suppress = suppressAns === 'y' || suppressAns === 'yes'; - rememberConsent(memKey, nowIso, suppress); - return { - confirmed_by_human: true, - understood: `用戶輸入資源名 "${ctx.resourceName}" 確認暴露${ctx.destination ? `(去向:${ctx.destination})` : ''}${suppress ? ';並選擇以後不再提醒' : ''}`, - confirmed_at: nowIso, - suppress_future: suppress, - }; - } finally { - rl.close(); - } -} - -/** 本機記住此資源已同意(避免下次重問;server 端仍獨立存法律憑證並強制) */ -function rememberConsent(memKey: string, confirmedAt: string, suppressFuture: boolean): void { - try { - const cfg = loadConfig(); - cfg.exposure_consented = cfg.exposure_consented ?? {}; - cfg.exposure_consented[memKey] = { confirmed_at: confirmedAt, suppress_future: suppressFuture }; - saveConfig(cfg); - } catch { - // 記不住不影響本次同意(server 端仍會擋首次) - } -} - -function printWarning(ctx: ExposureContext): void { - console.log(chalk.yellow.bold(`\n⚠️ 資料外流警示`)); - console.log(chalk.yellow(` 這個動作會把 ${ctx.kind} "${ctx.resourceName}" 變成可被外部呼叫。`)); - if (ctx.destination) { - console.log(chalk.gray(` 去向:${ctx.destination}`)); - } - if (ctx.dataSummary) { - console.log(chalk.gray(` 涉及資料:${ctx.dataSummary}`)); - } else { - console.log(chalk.gray(` 涉及資料:無法自動判斷,請自行確認此資源是否含敏感資料。`)); - } - console.log(chalk.gray(` 任何能呼叫它的人都能取得它的輸出/能力。`)); - console.log(''); - console.log(chalk.cyan(` arcrun 可幫你保護它:要求呼叫者帶 API Key/設權限/限流(一個動作就能加)。`)); - console.log(chalk.gray(` 若這是要公開的資料(如公開 API),可直接確認。`)); - console.log(''); -} diff --git a/cypher-executor/scripts/seed-api-recipes.ts b/cypher-executor/scripts/seed-api-recipes.ts index 8465391..8dd5d83 100644 --- a/cypher-executor/scripts/seed-api-recipes.ts +++ b/cypher-executor/scripts/seed-api-recipes.ts @@ -13,8 +13,7 @@ * ARCRUN_API_URL - 目標 cypher-executor,預設 https://cypher.arcrun.dev * ARCRUN_API_KEY - X-Arcrun-API-Key(POST /recipes 需要) * - * 注意:API recipe 帶 endpoint(資料去向)→ POST /recipes 會要 exposure_consent - * (data-exfil-warning)。seed 是平台預建、非用戶 push,腳本帶種子層級的 consent。 + * 注意:暴露 consent 閘已移除(leo 2026-06-29,Arcrun#13),POST /recipes 不再需要 consent。 * * 對應 SDD:.agents/specs/arcrun/sdk-and-website/self-hosted-init.md §5 */ @@ -49,14 +48,7 @@ async function main() { endpoint: recipe.endpoint, method: recipe.method, auth_service: recipe.auth_service, - // 種子層級的暴露同意:平台預建 recipe,非用戶互動 push。 - // 格式須符合 cypher-executor ExposureConsent(confirmed_by_human + understood + confirmed_at)。 - // 誠實標明來源是 seed,軌跡可審(mindset §7:機制價值是歸責+可審,非防偽)。 - exposure_consent: { - confirmed_by_human: true, - understood: `platform seed recipe (api-recipe-seeds.ts): ${recipe.canonical_id} → ${recipe.endpoint}`, - confirmed_at: new Date().toISOString(), - }, + // 暴露 consent 閘已移除(leo 2026-06-29,Arcrun#13):不再帶 exposure_consent。 }), }); diff --git a/cypher-executor/src/lib/exposure-consent.ts b/cypher-executor/src/lib/exposure-consent.ts deleted file mode 100644 index ba1d113..0000000 --- a/cypher-executor/src/lib/exposure-consent.ts +++ /dev/null @@ -1,60 +0,0 @@ -// 資料外流警示 — 同意憑證機制(data-exfil-warning SDD §7 法律憑證 + §1b API 層) -// -// 觸發策略(richblack):只在「資料變成可被外部呼叫」時要求同意(暴露面)。 -// webhook 部署(workflow 變對外 endpoint)、recipe push 都算。 -// -// 同意 = 法律憑證:留 log(誰、何時、同意了什麼),真出事時有「用戶明示知情同意」證據, -// 避免 arcrun 訴訟風險。「以後不要警示」(suppress_future)本身也 log。 -// -// 誠實限制:AI 能偽造 confirmed_by_human。本機制的價值是「法律歸責 + 可審」,不是技術防偽。 - -/** 暴露同意憑證(人類明示知情同意把某資源開放/送出) */ -export interface ExposureConsent { - confirmed_by_human: true; // 必須為 literal true - understood: string; // 人類說明「我知道這會把什麼開放給誰」(非空) - confirmed_at: string; // ISO timestamp - suppress_future?: boolean; // 「以後不要對此資源警示」(本選擇也 log) -} - -/** - * 判斷一個暴露動作是否已取得有效同意。 - * @param consent 本次請求帶的同意憑證 - * @param priorConsent 既有 record 裡存的同意(首次問、記住:§3) - * @returns null = 放行(已同意或已 suppress);string = 拒絕原因 - */ -export function checkExposureConsent( - consent: ExposureConsent | undefined, - priorConsent: ExposureConsent | undefined, -): string | null { - // 既有同意且選了「以後不警示」→ 放行(首次問記住) - if (priorConsent?.suppress_future) return null; - // 既有有效同意(同資源已確認過)→ 放行 - if (priorConsent?.confirmed_by_human === true) return null; - - // 本次請求帶了有效同意 → 放行 - if ( - consent?.confirmed_by_human === true && - typeof consent.understood === 'string' && - consent.understood.trim() !== '' - ) { - return null; - } - - return ( - '此動作會把資源變成可被外部呼叫(暴露/送出資料)。需人類明示同意。\n' + - '請用 CLI 互動確認(acr 會說明風險並提供保護選項),或帶 exposure_consent。\n' + - 'arcrun 可幫你保護:要求呼叫者帶 API Key / 設權限 / 限流。' - ); -} - -/** - * 正規化要存進 record 的同意憑證(法律憑證,可審)。 - * 優先用本次新同意,否則沿用既有。 - */ -export function resolveConsentForRecord( - consent: ExposureConsent | undefined, - priorConsent: ExposureConsent | undefined, -): ExposureConsent | undefined { - if (consent?.confirmed_by_human === true) return consent; - return priorConsent; -} diff --git a/cypher-executor/src/routes/init-seed.ts b/cypher-executor/src/routes/init-seed.ts index b9749f2..74c63e5 100644 --- a/cypher-executor/src/routes/init-seed.ts +++ b/cypher-executor/src/routes/init-seed.ts @@ -9,8 +9,7 @@ * 行為: * - 冪等:已存在的 recipe 直接覆寫(重跑安全)。 * - 一次灌「API recipe(API_RECIPE_SEEDS)+ auth recipe(AUTH_RECIPE_SEEDS)」兩者。 - * - 直接寫 KV(不走 POST /recipes 的 exposure_consent gate):種子是平台預建、非用戶互動 push, - * 帶 seed 層級的 consent 憑證(誠實標來源,軌跡可審;mindset §7:機制價值是歸責+可審非防偽)。 + * - 直接寫 KV:種子是平台預建、非用戶互動 push(暴露 consent 閘已於 Arcrun#13 移除)。 * - 誠實回報:逐筆 ok/fail 計數,不假綠。 * * 對應 SDD:.agents/specs/arcrun/sdk-and-website/self-hosted-init.md §5 @@ -28,11 +27,7 @@ export const initSeedRouter = new Hono<{ Bindings: Bindings }>(); initSeedRouter.post('/init/seed', async (c) => { const now = Date.now(); - const seedConsent = { - confirmed_by_human: true as const, - understood: 'platform seed (init/seed): 平台預建 recipe,非用戶互動 push', - confirmed_at: new Date(now).toISOString(), - }; + // 暴露 consent 閘已移除(leo 2026-06-29,Arcrun#13):種子不再帶 exposure_consent。 let apiOk = 0; let apiFail = 0; @@ -54,7 +49,6 @@ initSeedRouter.post('/init/seed', async (c) => { endpoint: seed.endpoint, method: (seed.method ?? 'POST').toUpperCase(), auth_service: seed.auth_service, - exposure_consent: existing?.exposure_consent ?? seedConsent, created_at: existing?.created_at ?? now, updated_at: now, }; diff --git a/cypher-executor/src/routes/recipes.ts b/cypher-executor/src/routes/recipes.ts index 76bed0c..6c67dbf 100644 --- a/cypher-executor/src/routes/recipes.ts +++ b/cypher-executor/src/routes/recipes.ts @@ -16,8 +16,6 @@ import { Hono } from 'hono'; import type { Bindings } from '../types'; import { deriveRecipeHash } from '../lib/hash'; -import { checkExposureConsent, resolveConsentForRecord } from '../lib/exposure-consent'; -import type { ExposureConsent } from '../lib/exposure-consent'; export const recipesRouter = new Hono<{ Bindings: Bindings }>(); @@ -46,9 +44,9 @@ export interface RecipeDefinition { key: string; inject_as: string; }>; - // 資料外流警示:recipe 定義一個資料去向(endpoint)。push 需人類明示同意(法律憑證)。 - // SDD: data-exfil-warning §7(公私一視同仁) - exposure_consent?: ExposureConsent; + // 暴露 consent 閘已移除(leo 2026-06-29 拍板,Arcrun#13):arcrun 是給 AI 用的系統, + // 不再對 push/暴露要求人類確認。此欄位保留只為向後相容舊 KV record(讀到不報錯,不再寫入/檢查)。 + exposure_consent?: unknown; created_at: number; updated_at: number; } @@ -104,11 +102,7 @@ recipesRouter.post('/recipes', async (c) => { // 讀取順序:先 UUID 模型(installed→uuid),fallback 舊 key(migration 前的種子)。 const existing = await resolveRecipe(canonicalId, c.env.RECIPES); - // 資料外流警示:recipe 定義資料去向(endpoint)。首次 push 需人類明示同意(公私一視同仁)。 - const consentError = checkExposureConsent(body.exposure_consent, existing?.exposure_consent); - if (consentError !== null) { - return c.json({ success: false, error: consentError, requires: 'exposure_consent' }, 403); - } + // 暴露 consent 閘已移除(leo 2026-06-29,Arcrun#13):直接 push,不攔。 const recipe: RecipeDefinition = { uuid: existing?.uuid ?? crypto.randomUUID(), @@ -124,7 +118,6 @@ recipesRouter.post('/recipes', async (c) => { body: body.body, auth_service: body.auth_service, credentials_required: body.credentials_required, - exposure_consent: resolveConsentForRecord(body.exposure_consent, existing?.exposure_consent), created_at: existing?.created_at ?? now, updated_at: now, }; @@ -162,11 +155,7 @@ recipesRouter.post('/recipes/submit', async (c) => { const hashId = await deriveRecipeHash(canonicalId); const now = Date.now(); - // 公共庫投稿一定是暴露 → 需明示同意(無同意直接擋)。投稿是新版本,不沿用既有同意。 - const consentError = checkExposureConsent(body.exposure_consent, undefined); - if (consentError !== null) { - return c.json({ success: false, error: consentError, requires: 'exposure_consent' }, 403); - } + // 暴露 consent 閘已移除(leo 2026-06-29,Arcrun#13):公共庫投稿不再需要人類確認。 // app-store 模型:**領新 uuid = 新增作者版本**,不覆蓋既有 canonical(§7.5.5)。 const recipe: RecipeDefinition = { @@ -183,7 +172,6 @@ recipesRouter.post('/recipes/submit', async (c) => { body: body.body, auth_service: body.auth_service, credentials_required: body.credentials_required, - exposure_consent: resolveConsentForRecord(body.exposure_consent, undefined), created_at: now, updated_at: now, }; diff --git a/cypher-executor/src/routes/webhooks-named.ts b/cypher-executor/src/routes/webhooks-named.ts index 4189aa0..3537ddc 100644 --- a/cypher-executor/src/routes/webhooks-named.ts +++ b/cypher-executor/src/routes/webhooks-named.ts @@ -30,8 +30,6 @@ import type { GraphNode } from '../types'; import { extractCronExpr } from '../lib/cron-match'; import { updateCronIndexEntry, CRON_INDEX_KEY } from '../lib/cron-index'; import { recordTelemetry } from '../lib/telemetry'; -import { checkExposureConsent, resolveConsentForRecord } from '../lib/exposure-consent'; -import type { ExposureConsent } from '../lib/exposure-consent'; export const webhooksNamedRouter = new Hono<{ Bindings: Bindings }>(); @@ -44,9 +42,8 @@ type NamedWorkflowRecord = { // 若首節點是 cron 零件,extract cron_expr 存進來供 scheduled() 比對 // 對應 SDD: arcrun.md 三-A P1 #3 cron_expr?: string; - // 資料外流警示:部署 webhook = 把 workflow 變對外可呼叫 endpoint(暴露面)。 - // 存人類明示同意憑證(法律憑證,可審)。SDD: data-exfil-warning §7 - exposure_consent?: ExposureConsent; + // 暴露 consent 閘已移除(leo 2026-06-29,Arcrun#13)。保留欄位只為向後相容舊 KV record。 + exposure_consent?: unknown; }; function kvKey(apiKey: string, name: string): string { @@ -103,7 +100,6 @@ webhooksNamedRouter.post('/webhooks/named', async (c) => { graph?: Record; config?: Record; description?: string; - exposure_consent?: ExposureConsent; } | null; if (!body?.name || !body.graph) { @@ -125,14 +121,7 @@ webhooksNamedRouter.post('/webhooks/named', async (c) => { return c.json({ error: 'workflow name 只能包含英文字母、數字、底線和連字號' }, 400); } - // 資料外流警示:部署 webhook = 把 workflow 變對外可呼叫 endpoint(暴露面)。 - // 首次部署某 workflow 需人類明示同意;已同意(含 suppress_future)則放行(§3 首次問記住)。 - const priorRaw = await c.env.WEBHOOKS.get(kvKey(apiKey, name)); - const priorRecord = priorRaw ? (JSON.parse(priorRaw) as NamedWorkflowRecord) : null; - const consentError = checkExposureConsent(body.exposure_consent, priorRecord?.exposure_consent); - if (consentError !== null) { - return c.json({ error: consentError, requires: 'exposure_consent' }, 403); - } + // 暴露 consent 閘已移除(leo 2026-06-29,Arcrun#13):部署 webhook 不再需要人類確認,直接放行。 // 偵測首節點是 cron 零件 → 抽 cron_expr 存進 record + 建輕量 index 給 scheduled() const cronExpr = extractCronExpr(body.graph); @@ -144,8 +133,6 @@ webhooksNamedRouter.post('/webhooks/named', async (c) => { description: body.description.trim(), // R1:已驗非空(見上),存 trim 後的值 created_at: new Date().toISOString(), cron_expr: cronExpr ?? undefined, - // 法律憑證:存人類明示同意(本次新同意或沿用既有) - exposure_consent: resolveConsentForRecord(body.exposure_consent, priorRecord?.exposure_consent), }; const start = Date.now();