fix(create): recover the atomic-byok-create commit dropped from #2617 (main lifecycle e2e is red) #2640
@@ -353,8 +353,14 @@ echo "--- Step 2: provision workspace (POST /workspaces) ---"
|
||||
# then trigger ONE clean provision via /restart. Seeding the volume is also what
|
||||
# makes the restart-survival assertion meaningful: the restart path reuses the
|
||||
# volume rather than any template.
|
||||
# core#2608: create is now ATOMIC for byok — the create-boundary gate
|
||||
# hard-rejects a byok model with no credential in scope, and the create-scope
|
||||
# vendor-key guard accepts the credential in the SAME payload (deriving from
|
||||
# the payload model instead of the not-yet-stored MODEL secret). So the dummy
|
||||
# key rides in the create body; the later flip+write steps remain as
|
||||
# idempotent belt-and-suspenders for the restart path.
|
||||
CREATE_BODY=$(cat <<JSON
|
||||
{"name":"Lifecycle E2E Stub","tier":2,"runtime":"$RUNTIME","model":"$LIFECYCLE_MODEL"}
|
||||
{"name":"Lifecycle E2E Stub","tier":2,"runtime":"$RUNTIME","model":"$LIFECYCLE_MODEL","secrets":{"$LIFECYCLE_LLM_KEY":"$LIFECYCLE_LLM_VALUE"}}
|
||||
JSON
|
||||
)
|
||||
RESP=$(admin_curl -X POST "$BASE/workspaces" -H "Content-Type: application/json" -d "$CREATE_BODY")
|
||||
|
||||
@@ -218,6 +218,15 @@ func validateBYOKCredentialSatisfiable(ctx context.Context, runtime, model strin
|
||||
if prov.IsPlatform() {
|
||||
return true, ""
|
||||
}
|
||||
// Atomic byok create: a payload secret the derived arm accepts satisfies
|
||||
// the gate outright — no DB work (create(model, secrets) stays one call).
|
||||
for _, want := range prov.AuthEnv {
|
||||
for _, have := range payloadSecretKeys {
|
||||
if have == want {
|
||||
return true, ""
|
||||
}
|
||||
}
|
||||
}
|
||||
// BYOK-derived: widen the auth context with the tenant's global secret
|
||||
// KEYS (names only, never values) and re-derive — a global key can both
|
||||
// flip the arm disambiguation and satisfy the requirement. A failed key
|
||||
|
||||
@@ -685,8 +685,27 @@ func (h *WorkspaceHandler) Create(c *gin.Context) {
|
||||
// Persist initial secrets from the create payload (inside same transaction).
|
||||
// nil/empty map is a no-op. Any failure rolls back the workspace insert
|
||||
// so we never have a workspace row without its intended secrets.
|
||||
//
|
||||
// Vendor-key guard, CREATE scope (core#2608 companion): the stored-state
|
||||
// resolver cannot see payload.Model yet (the MODEL secret persists after
|
||||
// commit), so a byok create would always misresolve platform_managed here
|
||||
// and reject the very credential the create-boundary gate REQUIRES in the
|
||||
// payload. Derive from the CREATE inputs instead: when (runtime, model,
|
||||
// payload keys) resolves a non-platform arm, vendor keys in this payload
|
||||
// are the atomic-byok-create shape and are allowed; platform-resolving
|
||||
// creates keep the full guard.
|
||||
allowVendorKeysForByokCreate := false
|
||||
if m, regErr := providerRegistry(); regErr == nil && m != nil {
|
||||
keys := make([]string, 0, len(payload.Secrets))
|
||||
for k := range payload.Secrets {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
if prov, dErr := m.DeriveProvider(payload.Runtime, strings.TrimSpace(payload.Model), keys); dErr == nil && !prov.IsPlatform() {
|
||||
allowVendorKeysForByokCreate = true
|
||||
}
|
||||
}
|
||||
for k, v := range payload.Secrets {
|
||||
if rejectPlatformManagedDirectLLMBypassForWorkspace(c, id, k) {
|
||||
if !allowVendorKeysForByokCreate && rejectPlatformManagedDirectLLMBypassForWorkspace(c, id, k) {
|
||||
tx.Rollback() //nolint:errcheck
|
||||
return
|
||||
}
|
||||
|
||||
@@ -522,11 +522,9 @@ func TestWorkspaceCreate_SecretPersistFails_RollsBack(t *testing.T) {
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectExec("INSERT INTO workspaces").
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
// Create() resolves billing mode per-workspace before the secret-strip gate.
|
||||
// An explicit byok override short-circuits the resolver (precedence 1) so the
|
||||
// OPENAI_API_KEY write is allowed and reaches the INSERT-and-fail path.
|
||||
mock.ExpectQuery(`SELECT llm_billing_mode FROM workspaces WHERE id = \$1`).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"llm_billing_mode"}).AddRow(LLMBillingModeBYOK))
|
||||
// core#2608 create-scope guard: the byok-form payload model derives a
|
||||
// non-platform arm from CREATE inputs (no DB read), so the vendor-key
|
||||
// write is allowed and reaches the INSERT-and-fail path directly.
|
||||
mock.ExpectExec("INSERT INTO workspace_secrets").
|
||||
WillReturnError(sql.ErrConnDone) // DB failure while writing secret
|
||||
mock.ExpectRollback() // workspace insert must be rolled back
|
||||
|
||||
Reference in New Issue
Block a user