feat(data-exfil-warning): 資料外流警示 — 暴露動作需人類明示同意
新 SDD .agents/specs/data-exfil-warning/(richblack review 過)。
觸發策略:只在「資料變成可被外部呼叫」時警示(webhook 部署 / recipe push),
不管出站打別人 API(高頻低風險)。
- C 同意憑證(exposure-consent.ts):ExposureConsent{confirmed_by_human, understood,
confirmed_at, suppress_future};同意=法律憑證,存 record 可審
- A API 層:webhook 部署 + recipe push 首次需 consent,缺→403;首次問記住(server 端)
- B CLI(exposure-warning.ts):仿 GCP 刪 project,要打資源名確認(比 y/n 硬);
--confirm-exposure(非互動)/ --suppress-warning(不再警示,本選擇也 log);
非 TTY 無旗標→拒絕(AI 不替人類確認暴露);本機 config 記住已同意(不重問)
- H hook:pre-bash 偵測 acr push/recipe push 無旗標→exit 2(creds push/run 不誤擋)
- 警示是「保護措施入口」:提示 arcrun 可幫加認證/權限/限流(資安優勢)
驗收:非 TTY 拒絕未送出(exit1)、hook 精準擋放、tsc 雙邊綠。
⚠️ A+B 必須一起 deploy(API 層擋 + CLI 帶 consent),否則 push 中間狀態壞。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,8 @@
|
||||
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 }>();
|
||||
|
||||
@@ -38,6 +40,9 @@ export interface RecipeDefinition {
|
||||
key: string;
|
||||
inject_as: string;
|
||||
}>;
|
||||
// 資料外流警示:recipe 定義一個資料去向(endpoint)。push 需人類明示同意(法律憑證)。
|
||||
// SDD: data-exfil-warning §7(公私一視同仁)
|
||||
exposure_consent?: ExposureConsent;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
}
|
||||
@@ -58,9 +63,15 @@ recipesRouter.post('/recipes', async (c) => {
|
||||
const hashId = await deriveRecipeHash(canonicalId);
|
||||
const now = Date.now();
|
||||
|
||||
// 讀取現有版本(保留 created_at)
|
||||
// 讀取現有版本(保留 created_at + 既有同意憑證)
|
||||
const existing = await c.env.RECIPES.get(`recipe:${canonicalId}`, 'json') as RecipeDefinition | null;
|
||||
|
||||
// 資料外流警示: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);
|
||||
}
|
||||
|
||||
const recipe: RecipeDefinition = {
|
||||
canonical_id: canonicalId,
|
||||
hash_id: hashId,
|
||||
@@ -72,6 +83,7 @@ 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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user