feat(exposure): 完全移除 acr push 暴露 consent 閘 (Arcrun#13 P1)
leo 2026-06-29 拍板:arcrun 是給 AI 用的系統,push/暴露不再需要人類確認。 - 刪 cypher-executor/src/lib/exposure-consent.ts(server 閘,MCP push 的真正擋點) - 刪 cli/src/lib/exposure-warning.ts(CLI 互動 + 非 TTY 拒絕) - recipes.ts / webhooks-named.ts:移除 checkExposureConsent 403 閘,直接放行 - recipe.ts / push.ts:移除 obtainExposureConsent 呼叫,不再 prompt/拒絕 - init-seed / seed-api-recipes:移除種子層級 consent - exposure_consent 欄位降為向後相容(讀舊 record 不報錯,不再寫入/檢查) 不補審計線索、不做替代防護(leo:先拿掉,出問題再設置)。 tsc 全綠(cypher-executor + cli)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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。
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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<string, unknown>;
|
||||
config?: Record<string, unknown>;
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user