diff --git a/cli/harness/hooks/arcrun-guard.sh b/cli/harness/hooks/arcrun-guard.sh index e279d31..cf7388a 100644 --- a/cli/harness/hooks/arcrun-guard.sh +++ b/cli/harness/hooks/arcrun-guard.sh @@ -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 - if [ ! -t 0 ] && [ "${ARCRUN_HUMAN_CONFIRMED:-}" != "1" ]; then - block "在非互動環境自動執行暴露動作(acr push / recipe push 會讓東西可被外部呼叫)" \ - "把這動作交給人類在終端機執行,或先讓使用者明示同意。不要替使用者決定公開。" + # 目標(壓測 §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 <\`。或使用者先在對話明示同意後親自於終端機執行。不要替使用者決定公開。" + fi fi fi diff --git a/cypher-executor/src/actions/auth-dispatcher.ts b/cypher-executor/src/actions/auth-dispatcher.ts index f9b1e69..e1de675 100644 --- a/cypher-executor/src/actions/auth-dispatcher.ts +++ b/cypher-executor/src/actions/auth-dispatcher.ts @@ -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 = { + 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(() => ''); diff --git a/cypher-executor/src/types.ts b/cypher-executor/src/types.ts index 698c309..ea4e5c9 100644 --- a/cypher-executor/src/types.ts +++ b/cypher-executor/src/types.ts @@ -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}) diff --git a/cypher-executor/wrangler.toml b/cypher-executor/wrangler.toml index b1ad7eb..b294da7 100644 --- a/cypher-executor/wrangler.toml +++ b/cypher-executor/wrangler.toml @@ -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)。