#!/usr/bin/env bash set -euo pipefail # Credential-slot qualification modes: # no arguments deterministic two-slot full-cycle smoke # --self-test-live-gate deterministic live gate and one-call proof # --live ... explicitly authorized representative one-shot MODE="deterministic" MODE_SET=0 SECRET_STDIN=0 LIVE_PROFILE="" LIVE_MODEL="" LIVE_REASONING_EFFORT="" LIVE_MAX_COMPLETION_TOKENS="" early_die() { printf '[credential-slot-smoke] ERROR: %s\n' "$*" >&2 exit 2 } # Parse and validate the complete mode boundary before creating temporary # files, reading stdin, building binaries, or opening a socket. while (($# > 0)); do case "$1" in --live) ((MODE_SET == 0)) || early_die "exactly one mode may be selected" MODE="live" MODE_SET=1 shift ;; --self-test-live-gate) ((MODE_SET == 0)) || early_die "exactly one mode may be selected" MODE="self-test-live-gate" MODE_SET=1 shift ;; --secret-stdin) SECRET_STDIN=1 shift ;; --profile) (($# >= 2)) || early_die "--profile requires a value" LIVE_PROFILE="$2" shift 2 ;; --model) (($# >= 2)) || early_die "--model requires a value" LIVE_MODEL="$2" shift 2 ;; --reasoning-effort) (($# >= 2)) || early_die "--reasoning-effort requires a value" LIVE_REASONING_EFFORT="$2" shift 2 ;; --max-completion-tokens) (($# >= 2)) || early_die "--max-completion-tokens requires a value" LIVE_MAX_COMPLETION_TOKENS="$2" shift 2 ;; *) early_die "unknown argument: $1" ;; esac done if [ "$MODE" = "live" ]; then [ "${IOP_ALLOW_LIVE_PROVIDER:-0}" = "1" ] || early_die "live mode requires IOP_ALLOW_LIVE_PROVIDER=1" [ "$SECRET_STDIN" = "1" ] || early_die "live mode requires --secret-stdin" [ -n "$LIVE_PROFILE" ] || early_die "live mode requires --profile" [ -n "$LIVE_MODEL" ] || early_die "live mode requires --model" case "$LIVE_REASONING_EFFORT" in minimal|low|medium|high|xhigh) ;; *) early_die "live mode requires a valid --reasoning-effort" ;; esac [[ "$LIVE_MAX_COMPLETION_TOKENS" =~ ^[1-9][0-9]*$ ]] || early_die "live mode requires a positive --max-completion-tokens" ((LIVE_MAX_COMPLETION_TOKENS <= 128)) || early_die "live mode max completion tokens must not exceed 128" elif [ "$SECRET_STDIN" = "1" ] || [ -n "$LIVE_PROFILE$LIVE_MODEL$LIVE_REASONING_EFFORT$LIVE_MAX_COMPLETION_TOKENS" ]; then early_die "live options require --live" fi SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" FIXTURE="$SCRIPT_DIR/fixtures/credential-slot-vendors.json" KEEP_TMP="${IOP_CREDENTIAL_SLOT_SMOKE_KEEP_TMP:-0}" HTTP_CONNECT_TIMEOUT=2 HTTP_PROBE_TIMEOUT=3 HTTP_COMPLETION_TIMEOUT=45 QUIET=0 TMP_DIR="" TEMP_PARENT="" HELPER_BIN="" FAKE_PID="" CP_PID="" EDGE_PID="" NODE_PID="" IOP_TOKEN="" CHAT_SECRET="" MESSAGES_SECRET="" ROTATED_SECRET="" log() { if [ "$QUIET" != "1" ]; then printf '[credential-slot-smoke] %s\n' "$*" fi } die() { printf '[credential-slot-smoke] ERROR: %s\n' "$*" >&2 exit 1 } redacted_tail() { local file="$1" line [ -s "$file" ] || return 0 printf '=== %s (sanitized tail) ===\n' "$(basename "$file")" >&2 while IFS= read -r line; do [ -z "$IOP_TOKEN" ] || line="${line//$IOP_TOKEN/[REDACTED_IOP_TOKEN]}" [ -z "$CHAT_SECRET" ] || line="${line//$CHAT_SECRET/[REDACTED_PROVIDER_SECRET]}" [ -z "$MESSAGES_SECRET" ] || line="${line//$MESSAGES_SECRET/[REDACTED_PROVIDER_SECRET]}" [ -z "$ROTATED_SECRET" ] || line="${line//$ROTATED_SECRET/[REDACTED_PROVIDER_SECRET]}" printf '%s\n' "$line" >&2 done < <(tail -n 60 "$file") } cleanup() { local rc=$? pid file deadline trap - EXIT INT TERM for pid in "$NODE_PID" "$EDGE_PID" "$CP_PID" "$FAKE_PID"; do if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then kill -TERM "$pid" 2>/dev/null || true fi done deadline=$((SECONDS + 5)) while ((SECONDS < deadline)); do local alive=0 for pid in "$NODE_PID" "$EDGE_PID" "$CP_PID" "$FAKE_PID"; do if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then alive=1 fi done [ "$alive" = "1" ] || break sleep 0.1 done for pid in "$NODE_PID" "$EDGE_PID" "$CP_PID" "$FAKE_PID"; do if [ -n "$pid" ]; then kill -KILL "$pid" 2>/dev/null || true wait "$pid" 2>/dev/null || true fi done if [ "$rc" -ne 0 ] && [ -n "$TMP_DIR" ] && [ -d "$TMP_DIR" ]; then for file in "$TMP_DIR"/{fake,control-plane,edge,edge-runtime,node}.log; do redacted_tail "$file" done printf '[credential-slot-smoke] failure evidence was sanitized before cleanup\n' >&2 fi if [ -n "$TMP_DIR" ] && [ -d "$TMP_DIR" ]; then if [ "$KEEP_TMP" = "1" ]; then printf '[credential-slot-smoke] retained temporary evidence: %s\n' "$TMP_DIR" >&2 else case "$TMP_DIR" in "$TEMP_PARENT"/iop-credential-slot-smoke.*) rm -rf -- "$TMP_DIR" ;; *) printf '[credential-slot-smoke] refused unsafe cleanup path: %s\n' "$TMP_DIR" >&2 ;; esac fi fi exit "$rc" } trap cleanup EXIT INT TERM require_tools() { local required for required in bash curl go jq openssl rg timeout; do command -v "$required" >/dev/null 2>&1 || die "$required is required" done [ -r "$FIXTURE" ] || die "fixture is not readable: $FIXTURE" } setup_workspace() { TEMP_PARENT="${TMPDIR:-/tmp}" [ -d "$TEMP_PARENT" ] || die "TMPDIR parent does not exist: $TEMP_PARENT" [ -w "$TEMP_PARENT" ] || die "TMPDIR parent is not writable: $TEMP_PARENT" TMP_DIR="$(mktemp -d "$TEMP_PARENT/iop-credential-slot-smoke.XXXXXX")" umask 077 mkdir -p "$TMP_DIR/go-tmp" "$TMP_DIR/artifacts" printf '#!/usr/bin/env sh\nexit 0\n' >"$TMP_DIR/executable-probe" chmod 700 "$TMP_DIR/executable-probe" if ! "$TMP_DIR/executable-probe"; then die "TMPDIR child is not executable; set TMPDIR to a writable executable parent" fi rm -f "$TMP_DIR/executable-probe" export GOTMPDIR="$TMP_DIR/go-tmp" } validate_fixture() { jq -e ' .profiles | length >= 4 and any(.[]; .id == "seulgi_chat" and .driver == "openai_chat" and .operation == "chat_completions" and .operation_path == "/v1/chat/completions" and .auth_header == "Authorization" and .auth_scheme == "Bearer") and any(.[]; .id == "seulgi_messages" and .driver == "anthropic_messages" and .operation == "messages" and .operation_path == "/v1/messages" and .auth_header == "x-api-key" and .auth_scheme == "") ' "$FIXTURE" >/dev/null || die "fixture profile contract is invalid" jq -e ' .routes | length == 2 and ([.[].upstream_model] | unique | length == 1) and ([.[].slot_alias] | unique | length == 2) and ([.[].route_alias] | unique | length == 2) and ([.[].provider_id] | unique | length == 2) ' "$FIXTURE" >/dev/null || die "fixture route contract is invalid" } write_helper_source() { cat >"$TMP_DIR/credential_smoke_helper.go" <<'GO' package main import ( "crypto/ecdh" "crypto/ed25519" "crypto/rand" "crypto/x509" "crypto/x509/pkix" "database/sql" "encoding/base64" "encoding/json" "encoding/pem" "flag" "fmt" "io" "math/big" "net/http" "net/url" "os" "path/filepath" "strings" "sync" "time" _ "modernc.org/sqlite" ) type profile struct { ID string `json:"id"` Vendor string `json:"vendor"` CredentialKind string `json:"credential_kind"` Driver string `json:"driver"` BaseURL string `json:"base_url"` UpstreamModel string `json:"upstream_model"` Operation string `json:"operation"` OperationPath string `json:"operation_path"` AuthHeader string `json:"auth_header"` AuthScheme string `json:"auth_scheme"` } type fixture struct { Profiles []profile `json:"profiles"` } func must(err error) { if err != nil { fmt.Fprintln(os.Stderr, err); os.Exit(1) } } func writeFile(path string, body []byte, mode os.FileMode) { must(os.WriteFile(path, body, mode)) } func randomBytes(size int) []byte { value := make([]byte, size) _, err := io.ReadFull(rand.Reader, value) must(err) return value } type caMaterial struct { cert *x509.Certificate; key ed25519.PrivateKey; pem []byte } func newCA(commonName string) caMaterial { pub, key, err := ed25519.GenerateKey(rand.Reader); must(err) now := time.Now().Add(-time.Minute) tmpl := &x509.Certificate{ SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: commonName}, NotBefore: now, NotAfter: now.Add(2*time.Hour), IsCA: true, BasicConstraintsValid: true, KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, } der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, pub, key); must(err) parsed, err := x509.ParseCertificate(der); must(err) return caMaterial{cert: parsed, key: key, pem: pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})} } func issue(dir, fileBase, role, name string, dns []string, ca caMaterial) { pub, key, err := ed25519.GenerateKey(rand.Reader); must(err) identity, err := url.Parse("spiffe://iop/"+role+"/"+name); must(err) serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 120)); must(err) now := time.Now().Add(-time.Minute) tmpl := &x509.Certificate{ SerialNumber: serial, Subject: pkix.Name{CommonName: name}, DNSNames: dns, URIs: []*url.URL{identity}, NotBefore: now, NotAfter: now.Add(2*time.Hour), KeyUsage: x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, } der, err := x509.CreateCertificate(rand.Reader, tmpl, ca.cert, pub, ca.key); must(err) keyDER, err := x509.MarshalPKCS8PrivateKey(key); must(err) writeFile(filepath.Join(dir, fileBase+".pem"), pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}), 0644) writeFile(filepath.Join(dir, fileBase+".key"), pem.EncodeToMemory(&pem.Block{Type: "PRIVATE"+" KEY", Bytes: keyDER}), 0600) } func material(dir string) { ca := newCA("IOP credential smoke CA") writeFile(filepath.Join(dir, "ca.pem"), ca.pem, 0644) other := newCA("IOP unrelated CA") writeFile(filepath.Join(dir, "other-ca.pem"), other.pem, 0644) issue(dir, "control-plane", "control-plane", "cp-smoke", []string{"cp.internal", "cp-api.internal"}, ca) issue(dir, "edge", "edge", "edge-smoke", []string{"edge.internal", "edge-api.internal"}, ca) issue(dir, "node", "node", "node-smoke", nil, ca) issue(dir, "wrong", "worker", "wrong-smoke", nil, ca) issuerPublic, issuerPrivate, err := ed25519.GenerateKey(rand.Reader); must(err) writeFile(filepath.Join(dir, "issuer.private"), []byte(base64.StdEncoding.EncodeToString(issuerPrivate)+"\n"), 0600) writeFile(filepath.Join(dir, "issuer.public"), []byte(base64.StdEncoding.EncodeToString(issuerPublic)+"\n"), 0644) recipient, err := ecdh.X25519().GenerateKey(rand.Reader); must(err) writeFile(filepath.Join(dir, "recipient.private"), []byte(base64.StdEncoding.EncodeToString(recipient.Bytes())+"\n"), 0600) keyMaterial := base64.StdEncoding.EncodeToString(randomBytes(32)) writeFile(filepath.Join(dir, "keyring.yaml"), []byte("keys:\n - id: smoke-key\n version: 1\n material: "+keyMaterial+"\n"), 0600) for _, name := range []string{"chat.secret", "messages.secret", "rotated.secret"} { value := "cred_"+base64.RawURLEncoding.EncodeToString(randomBytes(24)) writeFile(filepath.Join(dir, name), []byte(value), 0600) } } type counters struct { mu sync.Mutex Calls int `json:"calls"` ChatCalls int `json:"chat_calls"` MessagesCalls int `json:"messages_calls"` ChatAuthOK int `json:"chat_auth_ok"` MessagesAuthOK int `json:"messages_auth_ok"` OptionsOK int `json:"options_ok"` Unauthorized int `json:"unauthorized"` } func (c *counters) snapshot() map[string]int { c.mu.Lock(); defer c.mu.Unlock() return map[string]int{ "calls": c.Calls, "chat_calls": c.ChatCalls, "messages_calls": c.MessagesCalls, "chat_auth_ok": c.ChatAuthOK, "messages_auth_ok": c.MessagesAuthOK, "options_ok": c.OptionsOK, "unauthorized": c.Unauthorized, } } func readSecret(path string) string { value, err := os.ReadFile(path); must(err) return strings.TrimSpace(string(value)) } func expectedHeader(p profile, secret string) string { if strings.TrimSpace(p.AuthScheme) == "" { return secret } return strings.TrimSpace(p.AuthScheme)+" "+secret } func exactAuth(r *http.Request, p profile, secret string) bool { if r.Header.Get(p.AuthHeader) != expectedHeader(p, secret) { return false } if strings.EqualFold(p.AuthHeader, "Authorization") && r.Header.Get("x-api-key") != "" { return false } if strings.EqualFold(p.AuthHeader, "x-api-key") && r.Header.Get("Authorization") != "" { return false } return true } func loadFixture(path string) fixture { data, err := os.ReadFile(path); must(err) var value fixture must(json.Unmarshal(data, &value)) return value } func findProfile(value fixture, id string) profile { for _, candidate := range value.Profiles { if candidate.ID == id { return candidate } } panic("profile not found: "+id) } func fake(args []string) { fs := flag.NewFlagSet("fake", flag.ExitOnError) listen := fs.String("listen", "", "listen address") fixturePath := fs.String("fixture", "", "fixture path") chatProfileID := fs.String("chat-profile", "", "chat profile id") messagesProfileID := fs.String("messages-profile", "", "messages profile id") chatSecretPath := fs.String("chat-secret", "", "chat secret path") messagesSecretPath := fs.String("messages-secret", "", "messages secret path") model := fs.String("model", "", "expected model") reasoning := fs.String("reasoning-effort", "", "expected reasoning effort") maxTokens := fs.Int("max-completion-tokens", 0, "expected completion cap") must(fs.Parse(args)) value := loadFixture(*fixturePath) chatProfile := findProfile(value, *chatProfileID) var messagesProfile profile if *messagesProfileID != "" { messagesProfile = findProfile(value, *messagesProfileID) } state := &counters{} mux := http.NewServeMux() mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) }) mux.HandleFunc("/stats", func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(state.snapshot()) }) mux.HandleFunc("/v1/models", func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") _, _ = fmt.Fprintf(w, `{"object":"list","data":[{"id":%q}]}`, *model) }) chatHandler := func(w http.ResponseWriter, r *http.Request) { var request struct { Model string `json:"model"` Stream bool `json:"stream"` ReasoningEffort string `json:"reasoning_effort"` MaxCompletionTokens int `json:"max_completion_tokens"` } if r.Method != http.MethodPost || json.NewDecoder(io.LimitReader(r.Body, 1<<20)).Decode(&request) != nil || request.Model != *model || request.Stream { http.Error(w, "invalid request", http.StatusBadRequest); return } authOK := exactAuth(r, chatProfile, readSecret(*chatSecretPath)) optionsOK := *reasoning == "" || (request.ReasoningEffort == *reasoning && request.MaxCompletionTokens == *maxTokens) state.mu.Lock(); state.Calls++; state.ChatCalls++ if authOK { state.ChatAuthOK++ } else { state.Unauthorized++ } if optionsOK { state.OptionsOK++ } state.mu.Unlock() if !authOK { http.Error(w, "unauthorized", http.StatusUnauthorized); return } if !optionsOK { http.Error(w, "unexpected options", http.StatusBadRequest); return } w.Header().Set("Content-Type", "application/json") _, _ = fmt.Fprintf(w, `{"id":"chat-smoke","object":"chat.completion","model":%q,"choices":[{"index":0,"message":{"role":"assistant","content":"QUALIFIED"},"finish_reason":"stop"}],"usage":{"prompt_tokens":1,"completion_tokens":1,"total_tokens":2}}`, *model) } registered := map[string]bool{} register := func(path string, handler http.HandlerFunc) { if path != "" && !registered[path] { mux.HandleFunc(path, handler); registered[path] = true } } register(chatProfile.OperationPath, chatHandler) if parsed, err := url.Parse(chatProfile.BaseURL); err == nil && parsed.Path != "" { register(strings.TrimRight(parsed.Path, "/")+chatProfile.OperationPath, chatHandler) } // The live self-test overrides the endpoint to a /v1 base while retaining // the fixture operation /chat/completions. register("/v1/chat/completions", chatHandler) var messagesHandler http.HandlerFunc if *messagesProfileID != "" { messagesHandler = func(w http.ResponseWriter, r *http.Request) { var request struct { Model string `json:"model"`; Stream bool `json:"stream"` } if r.Method != http.MethodPost || json.NewDecoder(io.LimitReader(r.Body, 1<<20)).Decode(&request) != nil || request.Model != *model || request.Stream { http.Error(w, "invalid request", http.StatusBadRequest); return } authOK := exactAuth(r, messagesProfile, readSecret(*messagesSecretPath)) state.mu.Lock(); state.Calls++; state.MessagesCalls++ if authOK { state.MessagesAuthOK++ } else { state.Unauthorized++ } state.mu.Unlock() if !authOK { http.Error(w, "unauthorized", http.StatusUnauthorized); return } w.Header().Set("Content-Type", "application/json") _, _ = fmt.Fprintf(w, `{"id":"msg-smoke","type":"message","role":"assistant","model":%q,"content":[{"type":"text","text":"QUALIFIED"}],"stop_reason":"end_turn","usage":{"input_tokens":1,"output_tokens":1}}`, *model) } register(messagesProfile.OperationPath, messagesHandler) } mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if strings.HasSuffix(r.URL.Path, chatProfile.OperationPath) || strings.HasSuffix(r.URL.Path, "/chat/completions") { chatHandler(w, r); return } if messagesHandler != nil && (strings.HasSuffix(r.URL.Path, messagesProfile.OperationPath) || strings.HasSuffix(r.URL.Path, "/messages")) { messagesHandler(w, r); return } http.NotFound(w, r) }) server := &http.Server{Addr: *listen, Handler: mux, ReadHeaderTimeout: 5*time.Second} must(server.ListenAndServe()) } func inspectDB(path string) { db, err := sql.Open("sqlite", path+"?_pragma=busy_timeout(5000)"); must(err) defer db.Close() var slots, revisionRows, ciphertextRows int must(db.QueryRow(`SELECT COUNT(*), SUM(CASE WHEN length(ciphertext) > 16 AND length(nonce) > 0 AND key_id != '' THEN 1 ELSE 0 END) FROM credential_slots`).Scan(&slots, &ciphertextRows)) must(db.QueryRow(`SELECT COUNT(*) FROM credential_slot_revisions`).Scan(&revisionRows)) _ = json.NewEncoder(os.Stdout).Encode(map[string]int{"slots": slots, "revision_rows": revisionRows, "ciphertext_rows": ciphertextRows}) } func main() { if len(os.Args) < 2 { must(fmt.Errorf("helper command is required")) } switch os.Args[1] { case "material": if len(os.Args) != 3 { must(fmt.Errorf("material requires a directory")) } material(os.Args[2]) case "fake": fake(os.Args[2:]) case "inspect-db": if len(os.Args) != 3 { must(fmt.Errorf("inspect-db requires a path")) } inspectDB(os.Args[2]) default: must(fmt.Errorf("unknown helper command")) } } GO } build_helper() { write_helper_source HELPER_BIN="$TMP_DIR/credential-smoke-helper" (cd "$REPO_ROOT" && go build -o "$HELPER_BIN" "$TMP_DIR/credential_smoke_helper.go") chmod 700 "$HELPER_BIN" } build_runtime_binaries() { log "building fresh Control Plane, Edge, and Node binaries" (cd "$REPO_ROOT" && go build -o "$TMP_DIR/control-plane" ./apps/control-plane/cmd/control-plane) (cd "$REPO_ROOT" && go build -o "$TMP_DIR/iop-edge" ./apps/edge/cmd/edge) (cd "$REPO_ROOT" && go build -o "$TMP_DIR/iop-node" ./apps/node/cmd/node) chmod 700 "$TMP_DIR/control-plane" "$TMP_DIR/iop-edge" "$TMP_DIR/iop-node" } declare -a USED_PORTS=() pick_port() { local output_var="$1" base="$2" attempt=0 candidate used port while ((attempt < 200)); do ((attempt += 1)) candidate=$((base + RANDOM % 1500)) used=0 for port in "${USED_PORTS[@]}"; do [ "$port" = "$candidate" ] && used=1 done [ "$used" = "0" ] || continue if ! (echo >/dev/tcp/127.0.0.1/"$candidate") 2>/dev/null; then USED_PORTS+=("$candidate") printf -v "$output_var" '%s' "$candidate" return 0 fi done die "could not allocate a loopback port" } curl_timeout() { local max_time="$1" shift curl --noproxy '*' --connect-timeout "$HTTP_CONNECT_TIMEOUT" --max-time "$max_time" "$@" } wait_http() { local url="$1" label="$2" shift 2 local deadline=$((SECONDS + 35)) while ! curl_timeout "$HTTP_PROBE_TIMEOUT" -fsS "$@" "$url" >/dev/null 2>&1; do ((SECONDS < deadline)) || die "$label did not become ready" sleep 0.2 done } wait_fake() { wait_http "http://127.0.0.1:$FAKE_PORT/healthz" "fake provider" } start_process() { local output_var="$1" log_file="$2" shift 2 "$@" >"$log_file" 2>&1 & printf -v "$output_var" '%s' "$!" } prepare_material() { "$HELPER_BIN" material "$TMP_DIR" CHAT_SECRET="$(tr -d '\r\n' <"$TMP_DIR/chat.secret")" MESSAGES_SECRET="$(tr -d '\r\n' <"$TMP_DIR/messages.secret")" ROTATED_SECRET="$(tr -d '\r\n' <"$TMP_DIR/rotated.secret")" cp "$TMP_DIR/chat.secret" "$TMP_DIR/chat-current.secret" chmod 600 "$TMP_DIR/chat-current.secret" } start_fake_provider() { local chat_profile="$1" messages_profile="$2" model="$3" reasoning="${4:-}" max_tokens="${5:-0}" local args=(fake --listen "127.0.0.1:$FAKE_PORT" --fixture "$FIXTURE" --chat-profile "$chat_profile" --chat-secret "$TMP_DIR/chat-current.secret" --model "$model") if [ -n "$messages_profile" ]; then args+=(--messages-profile "$messages_profile" --messages-secret "$TMP_DIR/messages.secret") fi if [ -n "$reasoning" ]; then args+=(--reasoning-effort "$reasoning" --max-completion-tokens "$max_tokens") fi start_process FAKE_PID "$TMP_DIR/fake.log" "$HELPER_BIN" "${args[@]}" wait_fake } write_runtime_configs() { local chat_profile="$1" messages_profile="$2" upstream_model="$3" chat_provider="$4" messages_provider="$5" provider_endpoint="$6" local cp_config="$TMP_DIR/control-plane.yaml" edge_config="$TMP_DIR/edge.yaml" node_config="$TMP_DIR/node.yaml" DB_PATH="$TMP_DIR/credentials.db" cat >"$cp_config" <"$edge_config" <"$node_config" <>"$TMP_DIR/control-plane.log")" [ "${#IOP_TOKEN}" = "64" ] || die "principal bootstrap returned an invalid token" CP_BASE_URL="https://cp-api.internal:$CP_HTTP_PORT" EDGE_BASE_URL="https://edge-api.internal:$EDGE_OPENAI_PORT" CP_CURL=(--silent --show-error --cacert "$TMP_DIR/ca.pem" --resolve "cp-api.internal:$CP_HTTP_PORT:127.0.0.1") EDGE_CURL=(--silent --show-error --cacert "$TMP_DIR/ca.pem" --resolve "edge-api.internal:$EDGE_OPENAI_PORT:127.0.0.1") start_process CP_PID "$TMP_DIR/control-plane.log" "$TMP_DIR/control-plane" serve --config "$TMP_DIR/control-plane.yaml" wait_http "$CP_BASE_URL/healthz" "Control Plane HTTPS" "${CP_CURL[@]}" start_process EDGE_PID "$TMP_DIR/edge.log" "$TMP_DIR/iop-edge" serve --config "$TMP_DIR/edge.yaml" wait_http "$EDGE_BASE_URL/healthz" "Edge HTTPS ingress" "${EDGE_CURL[@]}" wait_http "$CP_BASE_URL/edges/edge-smoke" "Edge enrollment" "${CP_CURL[@]}" start_process NODE_PID "$TMP_DIR/node.log" "$TMP_DIR/iop-node" serve --config "$TMP_DIR/node.yaml" local deadline=$((SECONDS + 35)) while true; do if curl_timeout "$HTTP_PROBE_TIMEOUT" -fsS "${CP_CURL[@]}" "$CP_BASE_URL/edges/edge-smoke/status" >"$TMP_DIR/status.json" 2>/dev/null \ && jq -e '.nodes | any(.[]; .node_id == "node-smoke" and .connected == true)' "$TMP_DIR/status.json" >/dev/null; then rm -f "$TMP_DIR/status.json" break fi ((SECONDS < deadline)) || die "Node did not become dispatch-ready" sleep 0.2 done } json_text() { local expression="$1" jq -er "$expression" } create_slot_route() { local prefix="$1" profile_id="$2" vendor="$3" kind="$4" slot_alias="$5" route_alias="$6" provider_id="$7" upstream_model="$8" secret_file="$9" local slot_response slot_id route_body route_response route_id slot_revision route_revision slot_response="$(curl_timeout 15 "${CP_CURL[@]}" -fsS -X POST "$CP_BASE_URL/v1/credentials/slots" \ -H "Authorization: Bearer $IOP_TOKEN" \ -H "IOP-Credential-Vendor: $vendor" \ -H "IOP-Credential-Kind: $kind" \ -H "IOP-Credential-Alias: $slot_alias" \ -H 'Content-Type: application/octet-stream' --data-binary "@$secret_file")" slot_id="$(json_text '(.ID // .id)' <<<"$slot_response")" route_body="$(jq -cn --arg slot "$slot_id" --arg alias "$route_alias" --arg profile "$profile_id" --arg model "$upstream_model" --arg selector "$provider_id" '{slot_id:$slot,alias:$alias,profile_id:$profile,upstream_model:$model,resource_selector:$selector}')" route_response="$(curl_timeout 15 "${CP_CURL[@]}" -fsS -X POST "$CP_BASE_URL/v1/credentials/routes" \ -H "Authorization: Bearer $IOP_TOKEN" -H 'Content-Type: application/json' --data-binary "$route_body")" route_id="$(json_text '(.ID // .id)' <<<"$route_response")" route_revision="$(json_text '(.Revision // .revision)' <<<"$route_response")" slot_revision="$(curl_timeout 10 "${CP_CURL[@]}" -fsS "$CP_BASE_URL/v1/credentials/slots" -H "Authorization: Bearer $IOP_TOKEN" \ | jq -er --arg id "$slot_id" '.[] | select((.ID // .id) == $id) | (.Revision // .revision)')" printf -v "${prefix}_SLOT_ID" '%s' "$slot_id" printf -v "${prefix}_SLOT_REVISION" '%s' "$slot_revision" printf -v "${prefix}_ROUTE_ID" '%s' "$route_id" printf -v "${prefix}_ROUTE_REVISION" '%s' "$route_revision" } wait_route_projection() { local route_id="$1" local deadline=$((SECONDS + 20)) while true; do if curl_timeout "$HTTP_PROBE_TIMEOUT" -fsS "${EDGE_CURL[@]}" "$EDGE_BASE_URL/v1/models" -H "Authorization: Bearer $IOP_TOKEN" >"$TMP_DIR/models.json" 2>/dev/null \ && jq -e --arg id "$route_id" '.data | any(.[]; .id == $id)' "$TMP_DIR/models.json" >/dev/null; then rm -f "$TMP_DIR/models.json" return 0 fi ((SECONDS < deadline)) || die "managed route projection did not become visible" sleep 0.2 done } fake_stats() { curl_timeout "$HTTP_PROBE_TIMEOUT" -fsS "http://127.0.0.1:$FAKE_PORT/stats" } request_chat() { local route_alias="$1" output_file="$2" reasoning="${3:-}" max_tokens="${4:-0}" local body code body="$(jq -cn --arg model "$route_alias" --arg reasoning "$reasoning" --argjson cap "$max_tokens" ' {model:$model,messages:[{role:"user",content:"credential qualification"}],stream:false} + (if $reasoning == "" then {} else {reasoning_effort:$reasoning,max_completion_tokens:$cap} end) ')" code="$(curl_timeout "$HTTP_COMPLETION_TIMEOUT" "${EDGE_CURL[@]}" -o "$output_file" -w '%{http_code}' \ -X POST "$EDGE_BASE_URL/v1/chat/completions" -H "Authorization: Bearer $IOP_TOKEN" -H 'Content-Type: application/json' --data-binary "$body")" [ "$code" = "200" ] || die "Chat request returned HTTP $code" jq -e '.choices[0].finish_reason == "stop" and (.choices[0].message.content | type == "string" and length > 0)' "$output_file" >/dev/null \ || die "Chat response failed structural validation" rm -f "$output_file" } request_messages() { local route_alias="$1" output_file="$2" body code body="$(jq -cn --arg model "$route_alias" '{model:$model,max_tokens:64,messages:[{role:"user",content:"credential qualification"}],stream:false}')" code="$(curl_timeout "$HTTP_COMPLETION_TIMEOUT" "${EDGE_CURL[@]}" -o "$output_file" -w '%{http_code}' \ -X POST "$EDGE_BASE_URL/v1/messages" -H "x-api-key: $IOP_TOKEN" -H 'anthropic-version: 2023-06-01' -H 'Content-Type: application/json' --data-binary "$body")" [ "$code" = "200" ] || die "Messages request returned HTTP $code" jq -e '.type == "message" and .stop_reason == "end_turn" and ([.content[] | select(.type == "text") | .text] | join("") | length > 0)' "$output_file" >/dev/null \ || die "Messages response failed structural validation" rm -f "$output_file" } expect_tls_client_rejected() { local label="$1" port="$2" cert="$3" key="$4" server_name="$5" output output="$TMP_DIR/tls-$label.log" local -a args=(-brief -connect "127.0.0.1:$port" -servername "$server_name" -CAfile "$TMP_DIR/ca.pem") if [ -n "$cert" ]; then args+=(-cert "$cert" -key "$key") fi if timeout 5 openssl s_client "${args[@]}" "$output" 2>&1; then rg -qi 'alert|certificate required|handshake failure|peer workload identity mismatch' "$output" \ || die "$label unexpectedly completed an authenticated TLS handshake" fi } assert_tls_boundaries() { local code if curl_timeout 2 -fsS "http://127.0.0.1:$CP_HTTP_PORT/healthz" >/dev/null 2>&1; then die "credential HTTPS listener accepted plaintext HTTP" fi if curl_timeout 3 -fsS --cacert "$TMP_DIR/other-ca.pem" --resolve "cp-api.internal:$CP_HTTP_PORT:127.0.0.1" "$CP_BASE_URL/healthz" >/dev/null 2>&1; then die "credential HTTPS listener accepted an unrelated CA" fi code="$(curl_timeout 3 "${CP_CURL[@]}" -o /dev/null -w '%{http_code}' "$CP_BASE_URL/v1/credentials/slots")" [ "$code" = "401" ] || die "credential HTTPS operation without principal bearer returned HTTP $code" expect_tls_client_rejected "cp-no-cert" "$CP_EDGE_PORT" "" "" "cp.internal" expect_tls_client_rejected "cp-wrong-peer" "$CP_EDGE_PORT" "$TMP_DIR/wrong.pem" "$TMP_DIR/wrong.key" "cp.internal" expect_tls_client_rejected "edge-no-cert" "$EDGE_NODE_PORT" "" "" "edge.internal" expect_tls_client_rejected "edge-wrong-peer" "$EDGE_NODE_PORT" "$TMP_DIR/wrong.pem" "$TMP_DIR/wrong.key" "edge.internal" rm -f "$TMP_DIR"/tls-*.log } scan_sensitive_artifacts() { local -a candidates=( "$TMP_DIR/control-plane.yaml" "$TMP_DIR/edge.yaml" "$TMP_DIR/node.yaml" "$TMP_DIR/control-plane.log" "$TMP_DIR/edge.log" "$TMP_DIR/edge-runtime.log" "$TMP_DIR/node.log" "$TMP_DIR/fake.log" "$TMP_DIR/credentials.db" ) local -a files=() local value file for file in "${candidates[@]}"; do [ -f "$file" ] && files+=("$file") done for value in "$IOP_TOKEN" "$CHAT_SECRET" "$MESSAGES_SECRET" "$ROTATED_SECRET"; do if rg -a -F -l -- "$value" "${files[@]}" >/dev/null 2>&1; then die "sensitive value appeared in a persisted or captured artifact" fi done local private_key_pattern='BEGIN (AGE |OPENSSH |RSA |EC |ENCRYPTED )?PRIVATE KEY|AGE-SECRET-KEY-' printf '%s\n' '-----BEGIN PRIVATE KEY-----' | rg -a -q "$private_key_pattern" \ || die "private-key evidence matcher rejected its positive control" if rg -a -n "$private_key_pattern|lease[_-]?id|credential qualification|QUALIFIED" \ "${files[@]}" >/dev/null 2>&1; then die "private key, lease id, prompt, or provider body appeared in config/log evidence" fi } run_deterministic() { require_tools setup_workspace validate_fixture build_helper prepare_material build_runtime_binaries local chat_profile messages_profile chat_vendor messages_vendor chat_kind messages_kind local upstream_model chat_provider messages_provider chat_slot_alias messages_slot_alias chat_route_alias messages_route_alias chat_profile="$(jq -r '.routes[] | select(.id == "chat-slot-route") | .profile_id' "$FIXTURE")" messages_profile="$(jq -r '.routes[] | select(.id == "messages-slot-route") | .profile_id' "$FIXTURE")" chat_vendor="$(jq -r --arg id "$chat_profile" '.profiles[] | select(.id == $id) | .vendor' "$FIXTURE")" messages_vendor="$(jq -r --arg id "$messages_profile" '.profiles[] | select(.id == $id) | .vendor' "$FIXTURE")" chat_kind="$(jq -r --arg id "$chat_profile" '.profiles[] | select(.id == $id) | .credential_kind' "$FIXTURE")" messages_kind="$(jq -r --arg id "$messages_profile" '.profiles[] | select(.id == $id) | .credential_kind' "$FIXTURE")" upstream_model="$(jq -r '.routes[0].upstream_model' "$FIXTURE")" chat_provider="$(jq -r '.routes[] | select(.id == "chat-slot-route") | .provider_id' "$FIXTURE")" messages_provider="$(jq -r '.routes[] | select(.id == "messages-slot-route") | .provider_id' "$FIXTURE")" chat_slot_alias="$(jq -r '.routes[] | select(.id == "chat-slot-route") | .slot_alias' "$FIXTURE")" messages_slot_alias="$(jq -r '.routes[] | select(.id == "messages-slot-route") | .slot_alias' "$FIXTURE")" chat_route_alias="$(jq -r '.routes[] | select(.id == "chat-slot-route") | .route_alias' "$FIXTURE")" messages_route_alias="$(jq -r '.routes[] | select(.id == "messages-slot-route") | .route_alias' "$FIXTURE")" pick_port FAKE_PORT 41000 pick_port CP_HTTP_PORT 25000 pick_port CP_CLIENT_PORT 26500 pick_port CP_EDGE_PORT 28000 pick_port EDGE_NODE_PORT 30000 pick_port EDGE_BOOTSTRAP_PORT 32000 pick_port EDGE_OPENAI_PORT 34000 pick_port EDGE_METRICS_PORT 36000 start_fake_provider "$chat_profile" "$messages_profile" "$upstream_model" write_runtime_configs "$chat_profile" "$messages_profile" "$upstream_model" "$chat_provider" "$messages_provider" "http://127.0.0.1:$FAKE_PORT" start_managed_stack assert_tls_boundaries create_slot_route CHAT "$chat_profile" "$chat_vendor" "$chat_kind" "$chat_slot_alias" "$chat_route_alias" "$chat_provider" "$upstream_model" "$TMP_DIR/chat.secret" create_slot_route MESSAGES "$messages_profile" "$messages_vendor" "$messages_kind" "$messages_slot_alias" "$messages_route_alias" "$messages_provider" "$upstream_model" "$TMP_DIR/messages.secret" wait_route_projection "$CHAT_ROUTE_ID" wait_route_projection "$MESSAGES_ROUTE_ID" request_chat "$chat_route_alias" "$TMP_DIR/chat.response.json" request_messages "$messages_route_alias" "$TMP_DIR/messages.response.json" local stats initial_chat_auth initial_messages_auth stats="$(fake_stats)" initial_chat_auth="$(jq -r '.chat_auth_ok' <<<"$stats")" initial_messages_auth="$(jq -r '.messages_auth_ok' <<<"$stats")" [ "$initial_chat_auth" = "1" ] && [ "$initial_messages_auth" = "1" ] && [ "$(jq -r '.unauthorized' <<<"$stats")" = "0" ] \ || die "fake provider did not observe exact Chat and Messages authorization" local rotate_response rotated_revision rotate_response="$(curl_timeout 15 "${CP_CURL[@]}" -fsS -X POST "$CP_BASE_URL/v1/credentials/slots/$CHAT_SLOT_ID/rotate" \ -H "Authorization: Bearer $IOP_TOKEN" -H "IOP-Expected-Revision: $CHAT_SLOT_REVISION" \ -H 'Content-Type: application/octet-stream' --data-binary "@$TMP_DIR/rotated.secret")" rotated_revision="$(json_text '(.Revision // .revision)' <<<"$rotate_response")" cp "$TMP_DIR/rotated.secret" "$TMP_DIR/chat-current.secret.next" chmod 600 "$TMP_DIR/chat-current.secret.next" mv "$TMP_DIR/chat-current.secret.next" "$TMP_DIR/chat-current.secret" request_chat "$chat_route_alias" "$TMP_DIR/chat-rotated.response.json" stats="$(fake_stats)" [ "$(jq -r '.chat_calls' <<<"$stats")" = "2" ] && [ "$(jq -r '.chat_auth_ok' <<<"$stats")" = "2" ] \ || die "rotated credential revision did not reach the exact Chat auth boundary" wait_http "http://127.0.0.1:$EDGE_METRICS_PORT/healthz" "Edge metrics" curl_timeout 5 -fsS "http://127.0.0.1:$EDGE_METRICS_PORT/metrics" >"$TMP_DIR/metrics.txt" rg -F "credential_slot_ref=\"$CHAT_SLOT_ID\"" "$TMP_DIR/metrics.txt" >/dev/null || die "Chat slot attribution is missing" rg -F "credential_revision=\"$CHAT_SLOT_REVISION\"" "$TMP_DIR/metrics.txt" >/dev/null || die "initial credential revision attribution is missing" rg -F "credential_revision=\"$rotated_revision\"" "$TMP_DIR/metrics.txt" >/dev/null || die "rotated credential revision attribution is missing" for value in "$IOP_TOKEN" "$CHAT_SECRET" "$MESSAGES_SECRET" "$ROTATED_SECRET"; do ! rg -F -- "$value" "$TMP_DIR/metrics.txt" >/dev/null || die "sensitive value appeared in metrics" done ! rg -n 'lease[_-]?id|BEGIN .*PRIVATE KEY|credential qualification|QUALIFIED' "$TMP_DIR/metrics.txt" >/dev/null \ || die "unsafe attribution dimension appeared in metrics" rm -f "$TMP_DIR/metrics.txt" local db_stats db_stats="$("$HELPER_BIN" inspect-db "$DB_PATH")" [ "$(jq -r '.slots' <<<"$db_stats")" = "2" ] \ && [ "$(jq -r '.revision_rows' <<<"$db_stats")" = "3" ] \ && [ "$(jq -r '.ciphertext_rows' <<<"$db_stats")" = "2" ] \ || die "SQLite ciphertext/revision inspection failed: $db_stats" for value in "$CHAT_SECRET" "$MESSAGES_SECRET" "$ROTATED_SECRET"; do ! rg -a -F -- "$value" "$DB_PATH" >/dev/null || die "provider plaintext appeared in SQLite" done local pre_revoke_stats pre_revoke_tunnels revoke_response revoked_revision revoked_code post_revoke_stats post_revoke_tunnels pre_revoke_stats="$(fake_stats)" pre_revoke_tunnels="$(rg -c 'provider tunnel request received' "$TMP_DIR/node.log" || true)" revoke_response="$(curl_timeout 15 "${CP_CURL[@]}" -fsS -X POST "$CP_BASE_URL/v1/credentials/slots/$CHAT_SLOT_ID/revoke" \ -H "Authorization: Bearer $IOP_TOKEN" -H "IOP-Expected-Revision: $rotated_revision")" revoked_revision="$(json_text '(.Revision // .revision)' <<<"$revoke_response")" revoked_code="$(curl_timeout "$HTTP_COMPLETION_TIMEOUT" "${EDGE_CURL[@]}" -o "$TMP_DIR/revoked.response.json" -w '%{http_code}' \ -X POST "$EDGE_BASE_URL/v1/chat/completions" -H "Authorization: Bearer $IOP_TOKEN" -H 'Content-Type: application/json' \ --data-binary "$(jq -cn --arg model "$chat_route_alias" '{model:$model,messages:[{role:"user",content:"credential qualification"}],stream:false}')")" [[ "$revoked_code" != 2* ]] || die "post-revoke request unexpectedly succeeded" rm -f "$TMP_DIR/revoked.response.json" sleep 0.3 post_revoke_stats="$(fake_stats)" post_revoke_tunnels="$(rg -c 'provider tunnel request received' "$TMP_DIR/node.log" || true)" [ "$pre_revoke_stats" = "$post_revoke_stats" ] || die "post-revoke request changed upstream counters" [ "$pre_revoke_tunnels" = "$post_revoke_tunnels" ] || die "post-revoke request reached Node lease consumption" [ "$(jq -r '.messages_calls' <<<"$post_revoke_stats")" = "1" ] || die "post-revoke request fell back to the same-model Messages slot" scan_sensitive_artifacts jq -cn \ --arg chat_profile "$chat_profile" --arg messages_profile "$messages_profile" \ --arg chat_slot "$CHAT_SLOT_ID" --arg messages_slot "$MESSAGES_SLOT_ID" \ --argjson initial_revision "$CHAT_SLOT_REVISION" --argjson rotated_revision "$rotated_revision" --argjson revoked_revision "$revoked_revision" \ --argjson upstream_calls "$(jq -r '.calls' <<<"$post_revoke_stats")" --argjson lease_delivery_attempts "$post_revoke_tunnels" \ '{mode:"deterministic",profiles:[$chat_profile,$messages_profile],same_model_two_slot:true,exact_auth:{chat:2,messages:1},ciphertext_only:true,attribution:{chat_slot_ref:$chat_slot,messages_slot_ref:$messages_slot,initial_revision:$initial_revision,rotated_revision:$rotated_revision},tls_negative_matrix:"passed",post_revoke:{revision:$revoked_revision,lease_delivery_attempts:$lease_delivery_attempts,upstream_calls:$upstream_calls,counters_unchanged:true,no_fallback:true},result:"success"}' } run_live() { QUIET=1 require_tools setup_workspace validate_fixture build_helper prepare_material build_runtime_binaries local profile_json vendor kind driver base_url operation_path endpoint provider_id route_alias slot_alias secret_line profile_json="$(jq -ec --arg id "$LIVE_PROFILE" '.profiles[] | select(.id == $id)' "$FIXTURE")" || die "live profile is not present in the fixture" vendor="$(jq -r '.vendor' <<<"$profile_json")" kind="$(jq -r '.credential_kind' <<<"$profile_json")" driver="$(jq -r '.driver' <<<"$profile_json")" base_url="$(jq -r '.base_url' <<<"$profile_json")" operation_path="$(jq -r '.operation_path' <<<"$profile_json")" [ "$driver" = "openai_chat" ] || die "live one-shot currently requires an openai_chat profile" [ "$operation_path" = "/chat/completions" ] || die "live profile operation path is not approved" endpoint="$base_url" if [ "${IOP_CREDENTIAL_SLOT_LIVE_TEST:-0}" = "1" ]; then [ -n "${IOP_CREDENTIAL_SLOT_LIVE_TEST_ENDPOINT:-}" ] || die "live self-test endpoint is missing" endpoint="$IOP_CREDENTIAL_SLOT_LIVE_TEST_ENDPOINT" fi [[ "$endpoint" == https://* ]] || [ "${IOP_CREDENTIAL_SLOT_LIVE_TEST:-0}" = "1" ] \ || die "live provider endpoint must use HTTPS" secret_line="" IFS= read -r secret_line || [ -n "$secret_line" ] || die "live secret stdin was empty" [ -n "$secret_line" ] || die "live secret stdin was empty" printf '%s' "$secret_line" >"$TMP_DIR/live.secret" chmod 600 "$TMP_DIR/live.secret" CHAT_SECRET="$secret_line" cp "$TMP_DIR/live.secret" "$TMP_DIR/chat-current.secret" secret_line="" pick_port CP_HTTP_PORT 25000 pick_port CP_CLIENT_PORT 26500 pick_port CP_EDGE_PORT 28000 pick_port EDGE_NODE_PORT 30000 pick_port EDGE_BOOTSTRAP_PORT 32000 pick_port EDGE_OPENAI_PORT 34000 pick_port EDGE_METRICS_PORT 36000 provider_id="credential-live-provider" route_alias="credential-live-route" slot_alias="credential-live-slot" # A second unreachable provider is present only to keep the shared config # shape stable; the explicit resource selector makes it unreachable. write_runtime_configs "$LIVE_PROFILE" "$LIVE_PROFILE" "$LIVE_MODEL" "$provider_id" "credential-live-unselected" "$endpoint" start_managed_stack create_slot_route LIVE "$LIVE_PROFILE" "$vendor" "$kind" "$slot_alias" "$route_alias" "$provider_id" "$LIVE_MODEL" "$TMP_DIR/live.secret" wait_route_projection "$LIVE_ROUTE_ID" local body live_code body="$(jq -cn --arg model "$route_alias" --arg effort "$LIVE_REASONING_EFFORT" --argjson cap "$LIVE_MAX_COMPLETION_TOKENS" \ '{model:$model,messages:[{role:"user",content:"Reply with one word: qualified"}],stream:false,reasoning_effort:$effort,max_completion_tokens:$cap}')" live_code="$(curl_timeout "$HTTP_COMPLETION_TIMEOUT" "${EDGE_CURL[@]}" -o "$TMP_DIR/live.response.json" -w '%{http_code}' \ -X POST "$EDGE_BASE_URL/v1/chat/completions" -H "Authorization: Bearer $IOP_TOKEN" -H 'Content-Type: application/json' --data-binary "$body" \ 2>"$TMP_DIR/live.curl.stderr")" [ "$live_code" = "200" ] || die "live provider request returned HTTP $live_code" jq -e '.choices[0].finish_reason == "stop" and (.choices[0].message.content | type == "string" and length > 0)' "$TMP_DIR/live.response.json" >/dev/null \ || die "live provider response failed structural validation" rm -f "$TMP_DIR/live.response.json" [ ! -s "$TMP_DIR/live.curl.stderr" ] || die "live provider request produced stderr" rm -f "$TMP_DIR/live.curl.stderr" local live_revision live_revision="$(curl_timeout 10 "${CP_CURL[@]}" -fsS "$CP_BASE_URL/v1/credentials/slots" -H "Authorization: Bearer $IOP_TOKEN" \ | jq -er --arg id "$LIVE_SLOT_ID" '.[] | select((.ID // .id) == $id) | (.Revision // .revision)')" scan_sensitive_artifacts jq -cn --arg provider "$LIVE_PROFILE" --arg model "$LIVE_MODEL" --arg slot "$LIVE_SLOT_ID" \ --argjson revision "$live_revision" --arg date "$(date -u +%Y-%m-%d)" \ '{provider:$provider,model:$model,credential_slot_ref:$slot,credential_revision:$revision,date:$date,result:"success"}' } run_live_gate_self_test() { require_tools setup_workspace validate_fixture build_helper prepare_material pick_port FAKE_PORT 41000 start_fake_provider "openai" "" "gpt-5.6-luna" "high" 128 local before after fifo unread child_count_before child_count_after before="$(fake_stats)" fifo="$TMP_DIR/unread.fifo" mkfifo "$fifo" exec 9<>"$fifo" printf '%s\n' 'LIVE_GATE_INPUT_MUST_REMAIN_UNREAD' >&9 child_count_before="$(find "$TMP_DIR" -maxdepth 1 -type d -name 'iop-credential-slot-smoke.*' | wc -l | tr -d ' ')" if TMPDIR="$TMP_DIR" IOP_CREDENTIAL_SLOT_SMOKE_KEEP_TMP=0 IOP_ALLOW_LIVE_PROVIDER=0 IOP_CREDENTIAL_SLOT_LIVE_TEST=1 \ IOP_CREDENTIAL_SLOT_LIVE_TEST_ENDPOINT="http://127.0.0.1:$FAKE_PORT/v1" \ "$0" --live --secret-stdin --profile openai --model gpt-5.6-luna --reasoning-effort high --max-completion-tokens 128 \ <&9 >"$TMP_DIR/no-opt-in.stdout" 2>"$TMP_DIR/no-opt-in.stderr"; then die "live gate accepted a request without opt-in" fi IFS= read -r unread <&9 exec 9>&- rm -f "$fifo" [ "$unread" = "LIVE_GATE_INPUT_MUST_REMAIN_UNREAD" ] || die "live gate read stdin before rejecting opt-in" child_count_after="$(find "$TMP_DIR" -maxdepth 1 -type d -name 'iop-credential-slot-smoke.*' | wc -l | tr -d ' ')" [ "$child_count_before" = "$child_count_after" ] || die "live gate created a workspace before rejecting opt-in" rg -F 'live mode requires IOP_ALLOW_LIVE_PROVIDER=1' "$TMP_DIR/no-opt-in.stderr" >/dev/null \ || die "live gate did not emit the expected early rejection" [ ! -s "$TMP_DIR/no-opt-in.stdout" ] || die "live gate emitted stdout before rejecting opt-in" after="$(fake_stats)" [ "$before" = "$after" ] || die "live gate contacted the provider without opt-in" rm -f "$TMP_DIR/no-opt-in.stdout" "$TMP_DIR/no-opt-in.stderr" TMPDIR="$TMP_DIR" IOP_CREDENTIAL_SLOT_SMOKE_KEEP_TMP=0 IOP_ALLOW_LIVE_PROVIDER=1 IOP_CREDENTIAL_SLOT_LIVE_TEST=1 \ IOP_CREDENTIAL_SLOT_LIVE_TEST_ENDPOINT="http://127.0.0.1:$FAKE_PORT/v1" \ "$0" --live --secret-stdin --profile openai --model gpt-5.6-luna --reasoning-effort high --max-completion-tokens 128 \ <"$TMP_DIR/chat.secret" >"$TMP_DIR/authorized.stdout" 2>"$TMP_DIR/authorized.stderr" [ ! -s "$TMP_DIR/authorized.stderr" ] || die "authorized live self-test emitted stderr" jq -e ' (keys | sort) == ["credential_revision","credential_slot_ref","date","model","provider","result"] and .provider == "openai" and .model == "gpt-5.6-luna" and .credential_revision == 1 and .result == "success" ' "$TMP_DIR/authorized.stdout" >/dev/null || die "authorized live self-test emitted an invalid sanitized record" after="$(fake_stats)" [ "$(jq -r '.calls' <<<"$after")" = "1" ] \ && [ "$(jq -r '.chat_auth_ok' <<<"$after")" = "1" ] \ && [ "$(jq -r '.options_ok' <<<"$after")" = "1" ] \ && [ "$(jq -r '.unauthorized' <<<"$after")" = "0" ] \ || die "authorized live self-test did not make exactly one correct upstream call" ! rg -F -- "$CHAT_SECRET" "$TMP_DIR/authorized.stdout" "$TMP_DIR/authorized.stderr" >/dev/null \ || die "live self-test leaked its provider secret" rm -f "$TMP_DIR/authorized.stdout" "$TMP_DIR/authorized.stderr" jq -cn '{mode:"self-test-live-gate",zero_io_without_opt_in:true,stdin_unread_without_opt_in:true,authorized_upstream_calls:1,retries:0,forwarded_options:true,sanitized_output:true,result:"success"}' } case "$MODE" in deterministic) run_deterministic ;; self-test-live-gate) run_live_gate_self_test ;; live) run_live ;; *) early_die "invalid mode" ;; esac