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:
@@ -11,8 +11,9 @@ 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<void> {
|
||||
export async function cmdPush(filePath: string, options: { confirmExposure?: boolean; suppressWarning?: boolean } = {}): Promise<void> {
|
||||
const config = loadConfig();
|
||||
|
||||
if (config.mode === 'local') {
|
||||
@@ -89,6 +90,21 @@ export async function cmdPush(filePath: string): Promise<void> {
|
||||
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`,
|
||||
},
|
||||
options,
|
||||
);
|
||||
if (!consent) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// POST 至 /webhooks/named
|
||||
const deploySpinner = ora(`部署 "${workflow.name}" 至 ${executorUrl}`).start();
|
||||
try {
|
||||
@@ -100,6 +116,7 @@ export async function cmdPush(filePath: string): Promise<void> {
|
||||
graph,
|
||||
config: workflow.config ?? {},
|
||||
description: workflow.description ?? '',
|
||||
exposure_consent: consent ?? undefined,
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ interface RecipeDefinition {
|
||||
updated_at: number;
|
||||
}
|
||||
|
||||
export async function cmdRecipePush(filePath: string): Promise<void> {
|
||||
export async function cmdRecipePush(filePath: string, options: { confirmExposure?: boolean; suppressWarning?: boolean } = {}): Promise<void> {
|
||||
const config = loadConfig();
|
||||
|
||||
if (!config.api_key) {
|
||||
@@ -65,6 +65,21 @@ export async function cmdRecipePush(filePath: string): Promise<void> {
|
||||
}
|
||||
|
||||
const executorUrl = getCypherExecutorUrl(config);
|
||||
|
||||
// 資料外流警示:recipe 定義一個資料去向(endpoint)。首次 push 需人類明示同意(公私一視同仁)。
|
||||
// 已同意過(本機記住)→ 回非 null 自動放行;未同意/取消/非互動 → null → 中止。
|
||||
const consent = await obtainExposureConsent(
|
||||
{
|
||||
kind: 'recipe',
|
||||
resourceName: recipe.canonical_id,
|
||||
destination: recipe.endpoint,
|
||||
},
|
||||
options,
|
||||
);
|
||||
if (!consent) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const spinner = ora(`上傳 recipe "${recipe.canonical_id}"`).start();
|
||||
|
||||
try {
|
||||
@@ -74,7 +89,7 @@ export async function cmdRecipePush(filePath: string): Promise<void> {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Arcrun-API-Key': config.api_key,
|
||||
},
|
||||
body: JSON.stringify(recipe),
|
||||
body: JSON.stringify({ ...recipe, exposure_consent: consent ?? undefined }),
|
||||
});
|
||||
|
||||
const data = await res.json() as { success: boolean; recipe?: RecipeDefinition; error?: string };
|
||||
|
||||
Reference in New Issue
Block a user