feat(credential-injection): {{credential.X}} 用戶面語法(credential-primitives §8)
壓測 401 根因:{{credential.X}} 系統沒實裝,三條 template 展開路徑都不認
credential. namespace → 注入空值 → 目標 API 401(test_arcrun/5 Haiku 實證)。
修法(design §8,richblack 確認方向 B「讓 {{credential.X}} 真的能用」):
- auth_static_key 加 resolve_credentials action:給 names → WASM 內 kv_get +
crypto_decrypt → 回明文 map(不查 recipe、缺則誠實報錯)
- auth-dispatcher 加 resolveCredentialRefs:遞迴偵測 {{credential.X}} → 交 WASM
解密 → 回填(無 ref 則零開銷不打 WASM)
- graph-executor 在 node.data interpolate 後呼叫,不碰 ENCRYPTION_KEY(rule 02 §2.2)
解密全程在 WASM,TS 只偵測+回填。tinygo build OK + tsc 0 + §2.2 自檢綠。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -47,6 +47,9 @@ type Input struct {
|
||||
APIKey string `json:"api_key"`
|
||||
Service string `json:"service"`
|
||||
Request json.RawMessage `json:"request,omitempty"`
|
||||
// Names:resolve_credentials action 用——要解密的 credential 名稱清單
|
||||
// (用戶在 workflow node.data 寫 {{credential.NAME}} 時,graph-executor 收集後傳入)。
|
||||
Names []string `json:"names,omitempty"`
|
||||
}
|
||||
|
||||
type SecretRequirement struct {
|
||||
@@ -96,12 +99,20 @@ func main() {
|
||||
writeError("api_key 必填")
|
||||
return
|
||||
}
|
||||
|
||||
// resolve_credentials:用戶面 {{credential.NAME}} 入口。不查 recipe、不要求 service,
|
||||
// 直接給 names 解密回明文。在 service 必填檢查之前分流(只有 authenticate 才需要 recipe)。
|
||||
if input.Action == "resolve_credentials" {
|
||||
handleResolveCredentials(input)
|
||||
return
|
||||
}
|
||||
|
||||
if input.Service == "" {
|
||||
writeError("service 必填")
|
||||
return
|
||||
}
|
||||
if input.Action != "" && input.Action != "authenticate" {
|
||||
writeError("auth_static_key 僅支援 action=authenticate")
|
||||
writeError("auth_static_key 僅支援 action=authenticate / resolve_credentials")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -195,6 +206,52 @@ func main() {
|
||||
os.Stdout.Write(out)
|
||||
}
|
||||
|
||||
// handleResolveCredentials 處理用戶面 {{credential.NAME}} 入口:
|
||||
// 對每個 name 讀 {api_key}:cred:{name} + 解密,回傳明文 map。
|
||||
// 不查 auth recipe(與 authenticate 分流)。缺任一 name → success:false + error 指明(不假綠)。
|
||||
func handleResolveCredentials(input Input) {
|
||||
if len(input.Names) == 0 {
|
||||
writeError("resolve_credentials 需要 names(要解密的 credential 名稱清單)")
|
||||
return
|
||||
}
|
||||
|
||||
credentials := make(map[string]string, len(input.Names))
|
||||
for _, name := range input.Names {
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
kvKey := input.APIKey + ":cred:" + name
|
||||
encJSON, s := kvGet(kvKey)
|
||||
if s == 2 {
|
||||
writeError("缺少 credential: " + name + "。修復: 編輯 credentials.yaml 後執行 acr creds push")
|
||||
return
|
||||
}
|
||||
if s != 0 {
|
||||
writeError("kv_get 失敗(credential " + name + ")")
|
||||
return
|
||||
}
|
||||
|
||||
var rec EncryptedRecord
|
||||
if err := json.Unmarshal([]byte(encJSON), &rec); err != nil {
|
||||
writeError("credential " + name + " 格式錯誤: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
plaintext, ok := cryptoDecrypt(rec.Encrypted, rec.IV)
|
||||
if !ok {
|
||||
writeError("credential " + name + " 解密失敗")
|
||||
return
|
||||
}
|
||||
credentials[name] = plaintext
|
||||
}
|
||||
|
||||
out, _ := json.Marshal(map[string]interface{}{
|
||||
"success": true,
|
||||
"credentials": credentials,
|
||||
})
|
||||
os.Stdout.Write(out)
|
||||
}
|
||||
|
||||
// ── helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
func writeError(msg string) {
|
||||
|
||||
Reference in New Issue
Block a user