merge: guard hook 展示/執行區分 + auth primitive service binding(壓測 §9.5 + §11)
This commit is contained in:
@@ -28,10 +28,46 @@ block() {
|
||||
|
||||
# ── 硬擋:未經人類同意的暴露動作(明確越界,mindset §5)──────────────
|
||||
# 非互動環境下 CC 自己跑「部署對外 webhook / push recipe」= 替人類決定公開。
|
||||
#
|
||||
# 壓測 §9.5 修正(A6):舊版 grep "acr push" 連「在 echo/heredoc 裡**提到** push 字串」
|
||||
# 都擋 → CC 連「把可貼上的指令印給使用者看」都做不到,反而違反 §0.5「擋下必指正路」鐵則。
|
||||
# 故先判斷此命令是「展示指令」還是「真的執行 push」,只擋後者。
|
||||
#
|
||||
# 誠實限制(mindset §7):shell 命令層**無法 100% 乾淨區分**「執行」與「展示」
|
||||
# (例:`echo "跑 acr push"` 與 `acr push` 對單純 grep 都命中)。以下是**啟發式**,
|
||||
# 目的是降低誤殺、讓「印出正路指令」這條合法用途能通,不是完美防護。邊角情況可能漏判,
|
||||
# 但漏判方向是「偏向放行展示」——而真正的暴露(執行)仍受 TTY/env 把關。
|
||||
if echo "$CMD" | grep -qE "acr (push|recipe push)\b"; then
|
||||
# 目標(壓測 §9.5):擋「真的執行 push」,放行「把 push 指令印給使用者看」。
|
||||
#
|
||||
# 作法:把 heredoc 主體(cat/印出用的多行文字)先抽掉,剩下的才是「實際會被 shell 執行的命令列」,
|
||||
# 再看 push 是否出現在那裡的「命令位置」(行首 / ; & | && || 之後)。
|
||||
# 執行 → 命中:`acr push x`、`cd f && acr push x`、`echo done; acr push x`
|
||||
# 展示 → 不命中:`echo "跑 acr push x"`、`cat <<EOF\n acr push x \nEOF`(push 在 heredoc 主體內)
|
||||
#
|
||||
# 抽掉 heredoc 主體:刪掉從 `<<EOF`(或任何 <<TOKEN)那行的下一行起、到單獨一行 TOKEN 為止的內容。
|
||||
# 用 awk 做簡單狀態機(只處理最常見的 `<<TOKEN` / `<<-TOKEN`,不含複雜巢狀——夠用且誠實)。
|
||||
EXEC_PART=$(printf '%s\n' "$CMD" | awk '
|
||||
BEGIN{inhd=0}
|
||||
{
|
||||
if (inhd) { if ($0 ~ "^[[:space:]]*" term "[[:space:]]*$") { inhd=0 }; next }
|
||||
line=$0
|
||||
if (match(line, /<<-?[[:space:]]*"?'"'"'?[A-Za-z_][A-Za-z0-9_]*"?'"'"'?/)) {
|
||||
t=substr(line, RSTART, RLENGTH); gsub(/^<<-?[[:space:]]*["'"'"']?/, "", t); gsub(/["'"'"']$/, "", t)
|
||||
term=t; inhd=1
|
||||
}
|
||||
print line
|
||||
}')
|
||||
|
||||
# 誠實限制(mindset §7):shell 層無法 100% 乾淨區分「執行」與「展示」。awk heredoc 抽取只覆蓋
|
||||
# 常見形式(`<<EOF` / `<<-EOF` / 引號 token);`bash <<EOF ... acr push` 這種「heredoc 內容其實
|
||||
# 會被執行」的偷渡,這裡會誤放——但它已被上游安全分類器擋下(壓測 §9.6 實證)。取捨上偏向
|
||||
# 「放行展示」以保住 §0.5「擋下也要能印出正路指令」這條合法用途,不假裝此啟發式完美。
|
||||
if echo "$EXEC_PART" | grep -qE "(^|[;&|][[:space:]]*)acr[[:space:]]+(push|recipe[[:space:]]+push)\b"; then
|
||||
if [ ! -t 0 ] && [ "${ARCRUN_HUMAN_CONFIRMED:-}" != "1" ]; then
|
||||
block "在非互動環境自動執行暴露動作(acr push / recipe push 會讓東西可被外部呼叫)" \
|
||||
"把這動作交給人類在終端機執行,或先讓使用者明示同意。不要替使用者決定公開。"
|
||||
"交人類在終端機執行(真 TTY 會自動放行)。可把指令完整複製給使用者貼上自己跑:\`acr push <你的 workflow.yaml>\`。或使用者先在對話明示同意後親自於終端機執行。不要替使用者決定公開。"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
@@ -20,10 +20,23 @@
|
||||
import type { Bindings } from '../types';
|
||||
import { resolveAuthRecipe, resolveRecipe } from '../routes/recipes';
|
||||
import { wasmWorkerUrl } from '../lib/component-loader';
|
||||
import type { ServiceBinding } from '../types';
|
||||
|
||||
/** 對應 Phase 1-4 會部署的 auth primitive Worker */
|
||||
const SUPPORTED_PRIMITIVES = new Set(['static_key', 'service_account', 'oauth2']);
|
||||
|
||||
/**
|
||||
* primitive 名 → service binding key(Phase 7,2026-06-06)。
|
||||
* 比照 component-loader 的邏輯零件:有 binding 走 CF 內部 RPC(繞開同 zone 522 + 同帳號 workers.dev 1042),
|
||||
* 無 binding(如 self-hosted 未綁、或 mtls 未部署)fallback 到 fetch(workers.dev)。
|
||||
*/
|
||||
const AUTH_BINDING_MAP: Record<string, keyof import('../types').Bindings> = {
|
||||
static_key: 'SVC_AUTH_STATIC_KEY',
|
||||
service_account: 'SVC_AUTH_SERVICE_ACCOUNT',
|
||||
oauth2: 'SVC_AUTH_OAUTH2',
|
||||
mtls: 'SVC_AUTH_MTLS',
|
||||
};
|
||||
|
||||
/** auth primitive 本身的 componentId(避免自引用) */
|
||||
const AUTH_PRIMITIVE_IDS = new Set([
|
||||
'auth_static_key',
|
||||
@@ -62,18 +75,27 @@ export async function tryAuthDispatch(
|
||||
if (!recipe) return null;
|
||||
if (!SUPPORTED_PRIMITIVES.has(recipe.primitive)) return null;
|
||||
|
||||
// 走新路徑:HTTP POST 到對應 auth primitive Worker
|
||||
// 走 workers.dev 避開同 zone 死鎖(P0 #9)
|
||||
const primitiveUrl = wasmWorkerUrl(`auth_${recipe.primitive}`, env.WORKER_SUBDOMAIN);
|
||||
const res = await fetch(primitiveUrl, {
|
||||
// 呼叫對應 auth primitive Worker(Phase 7,2026-06-06):
|
||||
// binding 優先(CF 內部 RPC,繞開同 zone 522 + 同帳號 workers.dev 子請求 1042,壓測階段 11),
|
||||
// 無 binding(self-hosted 未綁 / mtls 未部署)fallback 到 fetch(workers.dev)。比照 component-loader makeLogicRunner。
|
||||
const reqInit = {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
action: 'authenticate',
|
||||
api_key: apiKey,
|
||||
service,
|
||||
}),
|
||||
});
|
||||
body: JSON.stringify({ action: 'authenticate', api_key: apiKey, service }),
|
||||
};
|
||||
|
||||
const bindingKey = AUTH_BINDING_MAP[recipe.primitive];
|
||||
const svc = bindingKey ? (env[bindingKey] as ServiceBinding | undefined) : undefined;
|
||||
|
||||
let res: Response;
|
||||
if (svc) {
|
||||
// service binding:用任意 URL,CF 內部 RPC 直送目標 Worker(不經公網)
|
||||
res = await svc.fetch(new Request('https://auth-primitive/', reqInit));
|
||||
} else {
|
||||
// fallback:公網 workers.dev(自架未綁 binding / 開發環境 / mtls)
|
||||
const primitiveUrl = wasmWorkerUrl(`auth_${recipe.primitive}`, env.WORKER_SUBDOMAIN);
|
||||
res = await fetch(primitiveUrl, reqInit);
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => '');
|
||||
|
||||
@@ -21,6 +21,12 @@ export type Bindings = {
|
||||
SVC_DATE_OPS: ServiceBinding;
|
||||
SVC_VALIDATE_JSON: ServiceBinding;
|
||||
// SVC_AI_TRANSFORM_* 已移除(Phase 2 刪 ai_transform 零件 + wrangler.toml service binding)
|
||||
// Auth primitive Service Bindings(Phase 7,2026-06-06):繞開 self-hosted 同帳號 workers.dev 子請求 1042。
|
||||
// optional:auth_mtls 尚未部署(無 binding);無 binding 時 auth-dispatcher fallback 到 fetch(workers.dev)。
|
||||
SVC_AUTH_STATIC_KEY?: ServiceBinding;
|
||||
SVC_AUTH_SERVICE_ACCOUNT?: ServiceBinding;
|
||||
SVC_AUTH_OAUTH2?: ServiceBinding;
|
||||
SVC_AUTH_MTLS?: ServiceBinding;
|
||||
// KV Context Store:節點 output 透過 KV 傳遞,解決同名欄位衝突
|
||||
EXEC_CONTEXT: KVNamespace;
|
||||
// Recipe Store:API recipe 定義(key: recipe:{canonical_id} 或 idx:{hash_id})
|
||||
|
||||
@@ -91,6 +91,24 @@ service = "arcrun-date-ops"
|
||||
binding = "SVC_VALIDATE_JSON"
|
||||
service = "arcrun-validate-json"
|
||||
|
||||
# Auth primitive service bindings(credential-primitives-wasm Phase 7,2026-06-06)
|
||||
# 為何:auth-dispatcher 原用 fetch(workers.dev) 打同帳號 auth worker,self-hosted 帳號踩 CF 1042
|
||||
# (壓測階段 11)。service binding 是 CF 內部 RPC,繞開同 zone 522 + 同帳號 1042。
|
||||
# 範圍:只綁「已部署」的 auth worker。auth_mtls 尚未部署(.component-builds 無、官方 404),
|
||||
# 綁不存在的 worker 會讓 deploy 報 "referenced Worker not found"(見上 ai_transform 教訓),
|
||||
# 故 mtls 待它部署後再加。auth-dispatcher 對無 binding 的 primitive 自動 fallback fetch。
|
||||
[[services]]
|
||||
binding = "SVC_AUTH_STATIC_KEY"
|
||||
service = "arcrun-auth-static-key"
|
||||
|
||||
[[services]]
|
||||
binding = "SVC_AUTH_SERVICE_ACCOUNT"
|
||||
service = "arcrun-auth-service-account"
|
||||
|
||||
[[services]]
|
||||
binding = "SVC_AUTH_OAUTH2"
|
||||
service = "arcrun-auth-oauth2"
|
||||
|
||||
# ai_transform_compile / ai_transform_run 已於 Phase 2(2026-05-29)刪除
|
||||
# (Arcrun 是 AI 呼叫的工具,工作流不該內嵌 AI 節點)。對應 worker 已 wrangler delete,
|
||||
# service binding 一併移除(否則 deploy 報 referenced Worker not found)。
|
||||
|
||||
Reference in New Issue
Block a user