mirror of
https://github.com/andrewyng/openworker.git
synced 2026-09-11 14:50:14 +00:00
Bedrock settings: one auth method at a time
'Connect with' segmented choice (API key / profile / IAM keys) shows only that method's fields; non-selected fields are dropped at build so stale values can't leak.
This commit is contained in:
@@ -12,11 +12,18 @@ An id with no family segment falls back to Converse as-is — Converse serves ev
|
|||||||
model (including Claude, minus the native extras), so a raw model id pasted without the
|
model (including Claude, minus the native extras), so a raw model id pasted without the
|
||||||
add-model dropdown still works.
|
add-model dropdown still works.
|
||||||
|
|
||||||
Credentials, in order:
|
Auth is ONE method at a time, selected by the profile's `auth_method` (a segmented choice
|
||||||
1. A **Bedrock API key** (bearer token from the Bedrock console — the no-CLI path). When
|
in Settings — owner call 2026-07-26, directness over field-precedence rules):
|
||||||
present (field or `AWS_BEARER_TOKEN_BEDROCK` env) it WINS over SigV4 credentials, the
|
|
||||||
same precedence boto3 applies; mixing both makes `AnthropicBedrock` raise outright.
|
- `api_key` — a **Bedrock API key** (bearer token from the console, the no-CLI path);
|
||||||
2. Explicit IAM keys → named profile (covers `aws sso login`) → ambient chain.
|
rides `AWS_BEARER_TOKEN_BEDROCK`, which boto3 prefers over SigV4 for Bedrock calls.
|
||||||
|
- `profile` — a named `~/.aws` profile (covers `aws sso login`); blank → the default
|
||||||
|
credential chain (env vars / ~/.aws / role).
|
||||||
|
- `iam` — explicit access keys (+ optional STS session token).
|
||||||
|
|
||||||
|
Fields from non-selected methods are dropped at construction, so a stale stored value can
|
||||||
|
never leak into a different auth path (`AnthropicBedrock` raises outright on a mix). A
|
||||||
|
missing/unknown method falls back to whichever fields are present, api_key first.
|
||||||
|
|
||||||
boto3 is a lazy import (packaged via the `bedrock` extra) and returns PLAIN DICTS — every
|
boto3 is a lazy import (packaged via the `bedrock` extra) and returns PLAIN DICTS — every
|
||||||
response/stream mapping here is dict-shaped, unlike the attribute objects other SDKs return.
|
response/stream mapping here is dict-shaped, unlike the attribute objects other SDKs return.
|
||||||
@@ -449,6 +456,7 @@ class BedrockProvider(ProviderClient):
|
|||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
region: Optional[str] = None,
|
region: Optional[str] = None,
|
||||||
|
auth_method: Optional[str] = None,
|
||||||
bedrock_api_key: Optional[str] = None,
|
bedrock_api_key: Optional[str] = None,
|
||||||
profile_name: Optional[str] = None,
|
profile_name: Optional[str] = None,
|
||||||
access_key_id: Optional[str] = None,
|
access_key_id: Optional[str] = None,
|
||||||
@@ -457,6 +465,14 @@ class BedrockProvider(ProviderClient):
|
|||||||
claude_client: Optional[ProviderClient] = None,
|
claude_client: Optional[ProviderClient] = None,
|
||||||
converse_client: Optional[ProviderClient] = None,
|
converse_client: Optional[ProviderClient] = None,
|
||||||
):
|
):
|
||||||
|
# Narrow to the selected auth method here, once — stale values stored under a
|
||||||
|
# previously-selected method must never reach a different credential path.
|
||||||
|
if auth_method == "api_key":
|
||||||
|
profile_name = access_key_id = secret_access_key = session_token = None
|
||||||
|
elif auth_method == "profile":
|
||||||
|
bedrock_api_key = access_key_id = secret_access_key = session_token = None
|
||||||
|
elif auth_method == "iam":
|
||||||
|
bedrock_api_key = profile_name = None
|
||||||
self._region = region
|
self._region = region
|
||||||
self._bedrock_api_key = bedrock_api_key
|
self._bedrock_api_key = bedrock_api_key
|
||||||
self._profile_name = profile_name
|
self._profile_name = profile_name
|
||||||
|
|||||||
@@ -43,6 +43,12 @@ class ProviderField:
|
|||||||
# Pre-filled (still editable) form value — e.g. an OpenAI-compatible vendor's official
|
# Pre-filled (still editable) form value — e.g. an OpenAI-compatible vendor's official
|
||||||
# endpoint, so the user only has to paste a key. Distinct from `placeholder` (grey hint).
|
# endpoint, so the user only has to paste a key. Distinct from `placeholder` (grey hint).
|
||||||
default: str = ""
|
default: str = ""
|
||||||
|
# Non-empty → the field renders as a segmented choice control instead of a text input;
|
||||||
|
# each option is {"value", "label"}. The chosen value is stored like any field value.
|
||||||
|
choices: tuple = ()
|
||||||
|
# {"other_field_key": "value"} → the field only renders while that other field holds
|
||||||
|
# that value. Drives auth-method switching (Bedrock) without a per-provider form.
|
||||||
|
show_when: Optional[dict] = None
|
||||||
|
|
||||||
def to_dict(self) -> dict[str, Any]:
|
def to_dict(self) -> dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
@@ -53,6 +59,8 @@ class ProviderField:
|
|||||||
"help": self.help,
|
"help": self.help,
|
||||||
"placeholder": self.placeholder,
|
"placeholder": self.placeholder,
|
||||||
"default": self.default,
|
"default": self.default,
|
||||||
|
"choices": [dict(c) for c in self.choices],
|
||||||
|
"show_when": self.show_when,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -140,6 +148,7 @@ def _build_bedrock(profile: dict[str, Any], secrets: Any) -> ProviderClient:
|
|||||||
|
|
||||||
return BedrockProvider(
|
return BedrockProvider(
|
||||||
region=get("region"),
|
region=get("region"),
|
||||||
|
auth_method=get("auth_method"),
|
||||||
bedrock_api_key=get("bedrock_api_key"),
|
bedrock_api_key=get("bedrock_api_key"),
|
||||||
profile_name=get("aws_profile"),
|
profile_name=get("aws_profile"),
|
||||||
access_key_id=get("aws_access_key_id"),
|
access_key_id=get("aws_access_key_id"),
|
||||||
@@ -300,44 +309,61 @@ DESCRIPTORS: list[ProviderDescriptor] = [
|
|||||||
placeholder="us-east-1",
|
placeholder="us-east-1",
|
||||||
help="The region your Bedrock model access is enabled in.",
|
help="The region your Bedrock model access is enabled in.",
|
||||||
),
|
),
|
||||||
|
# One auth method at a time (owner call 2026-07-26): AWS users are advanced —
|
||||||
|
# a direct choice beats a pile of "(optional)" fields with hidden precedence.
|
||||||
|
ProviderField(
|
||||||
|
"auth_method",
|
||||||
|
"Connect with",
|
||||||
|
secret=False,
|
||||||
|
required=False, # the default stands in; builder tolerates absence
|
||||||
|
default="api_key",
|
||||||
|
choices=(
|
||||||
|
{"value": "api_key", "label": "Bedrock API key"},
|
||||||
|
{"value": "profile", "label": "AWS profile"},
|
||||||
|
{"value": "iam", "label": "IAM keys"},
|
||||||
|
),
|
||||||
|
),
|
||||||
ProviderField(
|
ProviderField(
|
||||||
"bedrock_api_key",
|
"bedrock_api_key",
|
||||||
"Bedrock API key (optional)",
|
"Bedrock API key",
|
||||||
secret=True,
|
secret=True,
|
||||||
required=False,
|
required=False,
|
||||||
placeholder="ABSK…",
|
placeholder="ABSK…",
|
||||||
help="The easiest way in: generate one on the Bedrock console — no AWS "
|
show_when={"auth_method": "api_key"},
|
||||||
"CLI or IAM setup needed. Takes precedence over the fields below.",
|
help="Generate one on the Bedrock console — no AWS CLI or IAM setup needed.",
|
||||||
),
|
),
|
||||||
ProviderField(
|
ProviderField(
|
||||||
"aws_profile",
|
"aws_profile",
|
||||||
"AWS profile (optional)",
|
"AWS profile",
|
||||||
secret=False,
|
secret=False,
|
||||||
required=False,
|
required=False,
|
||||||
placeholder="default",
|
placeholder="default",
|
||||||
|
show_when={"auth_method": "profile"},
|
||||||
help="A named profile from ~/.aws — works with `aws configure` and "
|
help="A named profile from ~/.aws — works with `aws configure` and "
|
||||||
"`aws sso login` (IAM Identity Center). Leave blank to use explicit "
|
"`aws sso login` (IAM Identity Center). Leave blank to use your "
|
||||||
"keys below, or the default credential chain.",
|
"default AWS credentials (env vars or ~/.aws).",
|
||||||
),
|
),
|
||||||
ProviderField(
|
ProviderField(
|
||||||
"aws_access_key_id",
|
"aws_access_key_id",
|
||||||
"Access key ID (optional)",
|
"Access key ID",
|
||||||
secret=False,
|
secret=False,
|
||||||
required=False,
|
required=False,
|
||||||
placeholder="AKIA…",
|
placeholder="AKIA…",
|
||||||
|
show_when={"auth_method": "iam"},
|
||||||
),
|
),
|
||||||
ProviderField(
|
ProviderField(
|
||||||
"aws_secret_access_key",
|
"aws_secret_access_key",
|
||||||
"Secret access key (optional)",
|
"Secret access key",
|
||||||
secret=True,
|
secret=True,
|
||||||
required=False,
|
required=False,
|
||||||
|
show_when={"auth_method": "iam"},
|
||||||
),
|
),
|
||||||
ProviderField(
|
ProviderField(
|
||||||
"aws_session_token",
|
"aws_session_token",
|
||||||
"Session token (optional)",
|
"Session token (STS only, optional)",
|
||||||
secret=True,
|
secret=True,
|
||||||
required=False,
|
required=False,
|
||||||
help="Only for temporary credentials (STS).",
|
show_when={"auth_method": "iam"},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
build=_build_bedrock,
|
build=_build_bedrock,
|
||||||
@@ -560,19 +586,35 @@ def _verify_bedrock(fields: dict[str, Any], timeout: float) -> dict[str, Any]:
|
|||||||
"ok": False,
|
"ok": False,
|
||||||
"error": "boto3 is not installed — `pip install 'openworker[bedrock]'`.",
|
"error": "boto3 is not installed — `pip install 'openworker[bedrock]'`.",
|
||||||
}
|
}
|
||||||
|
# Exactly one auth method is exercised — the one the form has selected. Per-method
|
||||||
|
# required fields are checked here so the Test button says what's missing.
|
||||||
|
method = get("auth_method") or "api_key"
|
||||||
|
if method == "api_key" and not (
|
||||||
|
get("bedrock_api_key") or os.environ.get("AWS_BEARER_TOKEN_BEDROCK")
|
||||||
|
):
|
||||||
|
return {"ok": False, "error": "Enter a Bedrock API key to test."}
|
||||||
|
if method == "iam" and not (
|
||||||
|
get("aws_access_key_id") and get("aws_secret_access_key")
|
||||||
|
):
|
||||||
|
return {"ok": False, "error": "Enter an access key ID and secret access key."}
|
||||||
try:
|
try:
|
||||||
# A Bedrock API key rides the env var (boto3's only bearer channel) and then
|
if method == "api_key":
|
||||||
# wins over any SigV4 credentials, matching the provider's own precedence.
|
# The key rides the env var (boto3's only bearer channel); bearer then wins
|
||||||
if get("bedrock_api_key"):
|
# over any ambient SigV4 credentials for Bedrock calls.
|
||||||
os.environ["AWS_BEARER_TOKEN_BEDROCK"] = get("bedrock_api_key")
|
if get("bedrock_api_key"):
|
||||||
session = boto3.session.Session(
|
os.environ["AWS_BEARER_TOKEN_BEDROCK"] = get("bedrock_api_key")
|
||||||
**_session_kwargs(
|
session_kwargs: dict[str, Any] = {}
|
||||||
get("aws_profile"),
|
elif method == "profile":
|
||||||
|
# Blank profile → the default credential chain (env vars / ~/.aws / role).
|
||||||
|
session_kwargs = _session_kwargs(get("aws_profile"), None, None, None)
|
||||||
|
else: # iam
|
||||||
|
session_kwargs = _session_kwargs(
|
||||||
|
None,
|
||||||
get("aws_access_key_id"),
|
get("aws_access_key_id"),
|
||||||
get("aws_secret_access_key"),
|
get("aws_secret_access_key"),
|
||||||
get("aws_session_token"),
|
get("aws_session_token"),
|
||||||
)
|
)
|
||||||
)
|
session = boto3.session.Session(**session_kwargs)
|
||||||
client = session.client(
|
client = session.client(
|
||||||
"bedrock",
|
"bedrock",
|
||||||
region_name=get("region"),
|
region_name=get("region"),
|
||||||
|
|||||||
@@ -1265,6 +1265,8 @@ export interface ProviderField {
|
|||||||
help: string;
|
help: string;
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
default?: string; // pre-filled editable value (e.g. an OpenAI-compatible vendor's endpoint)
|
default?: string; // pre-filled editable value (e.g. an OpenAI-compatible vendor's endpoint)
|
||||||
|
choices?: { value: string; label: string }[]; // non-empty → segmented choice, not a text input
|
||||||
|
show_when?: Record<string, string> | null; // render only while these fields hold these values
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProviderInfo {
|
export interface ProviderInfo {
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
// Auth-method segmented choice + show_when field visibility (Bedrock's "Connect with"):
|
||||||
|
// only the selected method's fields render, and clicking a segment switches them.
|
||||||
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||||
|
import { ProviderForm, type ProviderSetupState } from "./ProviderSetup";
|
||||||
|
import type { ProviderInfo } from "../api";
|
||||||
|
|
||||||
|
vi.mock("../tauri", () => ({ openExternal: vi.fn() }));
|
||||||
|
|
||||||
|
afterEach(cleanup);
|
||||||
|
|
||||||
|
const BEDROCK: ProviderInfo = {
|
||||||
|
name: "bedrock",
|
||||||
|
title: "AWS Bedrock",
|
||||||
|
needs_key: true,
|
||||||
|
configured: false,
|
||||||
|
values: {},
|
||||||
|
suggested_models: [],
|
||||||
|
recommended_model: null,
|
||||||
|
fields: [
|
||||||
|
{ key: "region", label: "AWS region", secret: false, required: true, help: "", placeholder: "us-east-1" },
|
||||||
|
{
|
||||||
|
key: "auth_method",
|
||||||
|
label: "Connect with",
|
||||||
|
secret: false,
|
||||||
|
required: false,
|
||||||
|
help: "",
|
||||||
|
placeholder: "",
|
||||||
|
default: "api_key",
|
||||||
|
choices: [
|
||||||
|
{ value: "api_key", label: "Bedrock API key" },
|
||||||
|
{ value: "profile", label: "AWS profile" },
|
||||||
|
{ value: "iam", label: "IAM keys" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ key: "bedrock_api_key", label: "Bedrock API key", secret: true, required: false, help: "", placeholder: "ABSK…", show_when: { auth_method: "api_key" } },
|
||||||
|
{ key: "aws_profile", label: "AWS profile", secret: false, required: false, help: "", placeholder: "default", show_when: { auth_method: "profile" } },
|
||||||
|
{ key: "aws_secret_access_key", label: "Secret access key", secret: true, required: false, help: "", placeholder: "", show_when: { auth_method: "iam" } },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
function makePs(fields: Record<string, string>, setFieldValue = vi.fn()): ProviderSetupState {
|
||||||
|
return {
|
||||||
|
providers: [BEDROCK],
|
||||||
|
ordered: [BEDROCK],
|
||||||
|
refreshProviders: async () => {},
|
||||||
|
sel: "bedrock",
|
||||||
|
info: BEDROCK,
|
||||||
|
fields,
|
||||||
|
setFieldValue,
|
||||||
|
dirty: false,
|
||||||
|
verify: { state: "idle" },
|
||||||
|
showEndpoint: false,
|
||||||
|
setShowEndpoint: () => {},
|
||||||
|
keylessOk: new Set(),
|
||||||
|
credentialed: false,
|
||||||
|
savedState: false,
|
||||||
|
secretFilled: true,
|
||||||
|
openProvider: () => {},
|
||||||
|
backToGallery: () => {},
|
||||||
|
runTestAndSave: async () => true,
|
||||||
|
removeKey: async () => {},
|
||||||
|
cancelBackTimer: () => {},
|
||||||
|
statusFor: () => null,
|
||||||
|
saveField: async () => {},
|
||||||
|
fieldSaved: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("ProviderForm auth-method choice", () => {
|
||||||
|
it("renders only the selected method's fields", () => {
|
||||||
|
render(<ProviderForm ps={makePs({ auth_method: "api_key" })} tp="t" />);
|
||||||
|
expect(screen.getByTestId("t-field-bedrock_api_key")).toBeTruthy();
|
||||||
|
expect(screen.queryByTestId("t-field-aws_profile")).toBeNull();
|
||||||
|
expect(screen.queryByTestId("t-field-aws_secret_access_key")).toBeNull();
|
||||||
|
expect(screen.getByTestId("t-choice-auth_method-api_key").getAttribute("aria-checked")).toBe("true");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("switching the segment swaps the visible fields", () => {
|
||||||
|
const setFieldValue = vi.fn();
|
||||||
|
const { rerender } = render(
|
||||||
|
<ProviderForm ps={makePs({ auth_method: "api_key" }, setFieldValue)} tp="t" />,
|
||||||
|
);
|
||||||
|
fireEvent.click(screen.getByTestId("t-choice-auth_method-profile"));
|
||||||
|
expect(setFieldValue).toHaveBeenCalledWith("auth_method", "profile");
|
||||||
|
rerender(<ProviderForm ps={makePs({ auth_method: "profile" }, setFieldValue)} tp="t" />);
|
||||||
|
expect(screen.getByTestId("t-field-aws_profile")).toBeTruthy();
|
||||||
|
expect(screen.queryByTestId("t-field-bedrock_api_key")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("iam segment shows the key-pair fields", () => {
|
||||||
|
render(<ProviderForm ps={makePs({ auth_method: "iam" })} tp="t" />);
|
||||||
|
expect(screen.getByTestId("t-field-aws_secret_access_key")).toBeTruthy();
|
||||||
|
expect(screen.queryByTestId("t-field-bedrock_api_key")).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -341,6 +341,41 @@ export function ProviderForm({
|
|||||||
// A keyed provider's base_url is an expert option — it renders BELOW the key-help
|
// A keyed provider's base_url is an expert option — it renders BELOW the key-help
|
||||||
// line as its own advanced section (owner nit 2026-07-19), not inside the loop.
|
// line as its own advanced section (owner nit 2026-07-19), not inside the loop.
|
||||||
if (f.key === "base_url" && keyed) return null;
|
if (f.key === "base_url" && keyed) return null;
|
||||||
|
// Conditional fields (Bedrock's per-auth-method inputs) render only while their
|
||||||
|
// controlling field holds the matching value.
|
||||||
|
if (f.show_when && !Object.entries(f.show_when).every(([k, v]) => (ps.fields[k] || "") === v))
|
||||||
|
return null;
|
||||||
|
// Segmented choice (e.g. Bedrock's "Connect with"): a button per option, stored
|
||||||
|
// like any field value. Switching methods requires a fresh Test to save.
|
||||||
|
if (f.choices?.length)
|
||||||
|
return (
|
||||||
|
<div key={f.key}>
|
||||||
|
<label className={label}>{f.label}</label>
|
||||||
|
<div className="flex gap-1.5" role="radiogroup" aria-label={f.label}>
|
||||||
|
{f.choices.map((c) => {
|
||||||
|
const active = (ps.fields[f.key] || "") === c.value;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={c.value}
|
||||||
|
role="radio"
|
||||||
|
aria-checked={active}
|
||||||
|
className={
|
||||||
|
"px-3 py-1.5 rounded-lg border text-[12.5px] transition-colors " +
|
||||||
|
(active
|
||||||
|
? "border-accent text-ink font-medium"
|
||||||
|
: "border-line text-muted hover:border-lineStrong")
|
||||||
|
}
|
||||||
|
data-testid={`${tp}-choice-${f.key}-${c.value}`}
|
||||||
|
onClick={() => ps.setFieldValue(f.key, c.value)}
|
||||||
|
>
|
||||||
|
{c.label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{f.help && <p className="text-[11.5px] text-faint mt-1">{f.help}</p>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
// Test lives next to the required secret (the API key) when there is one; cloud
|
// Test lives next to the required secret (the API key) when there is one; cloud
|
||||||
// providers whose secrets are all optional (Bedrock, Vertex) test from the first
|
// providers whose secrets are all optional (Bedrock, Vertex) test from the first
|
||||||
// field instead — their credentials may be ambient (~/.aws, ADC).
|
// field instead — their credentials may be ambient (~/.aws, ADC).
|
||||||
|
|||||||
@@ -312,6 +312,32 @@ def test_claude_family_prefers_bedrock_api_key_over_sigv4(monkeypatch):
|
|||||||
assert sub._client.aws_profile is None
|
assert sub._client.aws_profile is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_auth_method_narrows_out_other_methods_fields(monkeypatch):
|
||||||
|
"""Stale values stored under a previously-selected method must never leak into a
|
||||||
|
different auth path — the selected method drops everything else at construction."""
|
||||||
|
monkeypatch.delenv("AWS_BEARER_TOKEN_BEDROCK", raising=False)
|
||||||
|
p = BedrockProvider(
|
||||||
|
region="us-east-1",
|
||||||
|
auth_method="profile",
|
||||||
|
bedrock_api_key="ABSKstale",
|
||||||
|
profile_name="work",
|
||||||
|
access_key_id="AKIAstale",
|
||||||
|
secret_access_key="stale",
|
||||||
|
)
|
||||||
|
assert p._bedrock_api_key is None
|
||||||
|
assert p._access_key_id is None
|
||||||
|
sub = p._family_client("claude")
|
||||||
|
assert sub._client.api_key is None
|
||||||
|
assert sub._client.aws_profile == "work"
|
||||||
|
|
||||||
|
p2 = BedrockProvider(
|
||||||
|
region="us-east-1", auth_method="api_key", bedrock_api_key="ABSKlive",
|
||||||
|
profile_name="stale",
|
||||||
|
)
|
||||||
|
assert p2._profile_name is None
|
||||||
|
assert p2._family_client("claude")._client.api_key == "ABSKlive"
|
||||||
|
|
||||||
|
|
||||||
def test_converse_client_publishes_api_key_as_bearer_env(monkeypatch):
|
def test_converse_client_publishes_api_key_as_bearer_env(monkeypatch):
|
||||||
import os
|
import os
|
||||||
|
|
||||||
@@ -369,6 +395,7 @@ def test_bedrock_descriptor_and_builder():
|
|||||||
keys = [f.key for f in d.fields]
|
keys = [f.key for f in d.fields]
|
||||||
assert keys == [
|
assert keys == [
|
||||||
"region",
|
"region",
|
||||||
|
"auth_method",
|
||||||
"bedrock_api_key",
|
"bedrock_api_key",
|
||||||
"aws_profile",
|
"aws_profile",
|
||||||
"aws_access_key_id",
|
"aws_access_key_id",
|
||||||
@@ -378,6 +405,16 @@ def test_bedrock_descriptor_and_builder():
|
|||||||
assert [f.key for f in d.fields if f.required] == ["region"]
|
assert [f.key for f in d.fields if f.required] == ["region"]
|
||||||
secret = {f.key for f in d.fields if f.secret}
|
secret = {f.key for f in d.fields if f.secret}
|
||||||
assert secret == {"bedrock_api_key", "aws_secret_access_key", "aws_session_token"}
|
assert secret == {"bedrock_api_key", "aws_secret_access_key", "aws_session_token"}
|
||||||
|
# One auth method at a time: a defaulted segmented choice drives per-method fields.
|
||||||
|
method = next(f for f in d.fields if f.key == "auth_method")
|
||||||
|
assert method.default == "api_key"
|
||||||
|
assert [c["value"] for c in method.choices] == ["api_key", "profile", "iam"]
|
||||||
|
by_key = {f.key: f for f in d.fields}
|
||||||
|
assert by_key["bedrock_api_key"].show_when == {"auth_method": "api_key"}
|
||||||
|
assert by_key["aws_profile"].show_when == {"auth_method": "profile"}
|
||||||
|
assert by_key["aws_secret_access_key"].show_when == {"auth_method": "iam"}
|
||||||
|
assert by_key["region"].show_when is None
|
||||||
|
assert by_key["region"].to_dict()["choices"] == []
|
||||||
# Recommended model is curated in the matrix (set_provider's auto-add depends on it).
|
# Recommended model is curated in the matrix (set_provider's auto-add depends on it).
|
||||||
from coworker.providers.matrix import models_for_provider
|
from coworker.providers.matrix import models_for_provider
|
||||||
|
|
||||||
@@ -444,7 +481,8 @@ def test_verify_bedrock_ok(monkeypatch):
|
|||||||
captured: dict = {}
|
captured: dict = {}
|
||||||
_patch_session(monkeypatch, _FakeBedrockControl(), captured)
|
_patch_session(monkeypatch, _FakeBedrockControl(), captured)
|
||||||
out = verify_provider_key(
|
out = verify_provider_key(
|
||||||
"bedrock", fields={"region": "us-east-1", "aws_profile": "work"}
|
"bedrock",
|
||||||
|
fields={"region": "us-east-1", "auth_method": "profile", "aws_profile": "work"},
|
||||||
)
|
)
|
||||||
assert out == {"ok": True}
|
assert out == {"ok": True}
|
||||||
assert captured["service"] == "bedrock"
|
assert captured["service"] == "bedrock"
|
||||||
@@ -452,6 +490,52 @@ def test_verify_bedrock_ok(monkeypatch):
|
|||||||
assert captured["client"]["region_name"] == "us-east-1"
|
assert captured["client"]["region_name"] == "us-east-1"
|
||||||
|
|
||||||
|
|
||||||
|
def test_verify_bedrock_per_method_required_fields(monkeypatch):
|
||||||
|
from coworker.providers.registry import verify_provider_key
|
||||||
|
|
||||||
|
monkeypatch.delenv("AWS_BEARER_TOKEN_BEDROCK", raising=False)
|
||||||
|
out = verify_provider_key(
|
||||||
|
"bedrock", fields={"region": "us-east-1", "auth_method": "api_key"}
|
||||||
|
)
|
||||||
|
assert not out["ok"] and "Bedrock API key" in out["error"]
|
||||||
|
out = verify_provider_key(
|
||||||
|
"bedrock",
|
||||||
|
fields={"region": "us-east-1", "auth_method": "iam", "aws_access_key_id": "AKIA"},
|
||||||
|
)
|
||||||
|
assert not out["ok"] and "secret access key" in out["error"]
|
||||||
|
# Blank profile is fine — it means the default credential chain.
|
||||||
|
captured: dict = {}
|
||||||
|
_patch_session(monkeypatch, _FakeBedrockControl(), captured)
|
||||||
|
out = verify_provider_key(
|
||||||
|
"bedrock", fields={"region": "us-east-1", "auth_method": "profile"}
|
||||||
|
)
|
||||||
|
assert out == {"ok": True}
|
||||||
|
assert captured["session"] == {}
|
||||||
|
|
||||||
|
|
||||||
|
def test_verify_bedrock_ignores_other_methods_stale_fields(monkeypatch):
|
||||||
|
from coworker.providers.registry import verify_provider_key
|
||||||
|
|
||||||
|
monkeypatch.delenv("AWS_BEARER_TOKEN_BEDROCK", raising=False)
|
||||||
|
captured: dict = {}
|
||||||
|
_patch_session(monkeypatch, _FakeBedrockControl(), captured)
|
||||||
|
out = verify_provider_key(
|
||||||
|
"bedrock",
|
||||||
|
fields={
|
||||||
|
"region": "us-east-1",
|
||||||
|
"auth_method": "iam",
|
||||||
|
"aws_access_key_id": "AKIA1",
|
||||||
|
"aws_secret_access_key": "sec",
|
||||||
|
"aws_profile": "stale", # other method's leftover — must not be used
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert out == {"ok": True}
|
||||||
|
assert captured["session"] == {
|
||||||
|
"aws_access_key_id": "AKIA1",
|
||||||
|
"aws_secret_access_key": "sec",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_verify_bedrock_api_key_rides_the_bearer_env(monkeypatch):
|
def test_verify_bedrock_api_key_rides_the_bearer_env(monkeypatch):
|
||||||
import os
|
import os
|
||||||
|
|
||||||
@@ -478,7 +562,9 @@ def test_verify_bedrock_maps_client_errors(monkeypatch):
|
|||||||
"ListFoundationModels",
|
"ListFoundationModels",
|
||||||
)
|
)
|
||||||
_patch_session(monkeypatch, _FakeBedrockControl(exc=denied), {})
|
_patch_session(monkeypatch, _FakeBedrockControl(exc=denied), {})
|
||||||
out = verify_provider_key("bedrock", fields={"region": "us-east-1"})
|
out = verify_provider_key(
|
||||||
|
"bedrock", fields={"region": "us-east-1", "auth_method": "profile"}
|
||||||
|
)
|
||||||
assert not out["ok"] and "Bedrock access" in out["error"]
|
assert not out["ok"] and "Bedrock access" in out["error"]
|
||||||
|
|
||||||
bad_key = ClientError(
|
bad_key = ClientError(
|
||||||
@@ -486,5 +572,7 @@ def test_verify_bedrock_maps_client_errors(monkeypatch):
|
|||||||
"ListFoundationModels",
|
"ListFoundationModels",
|
||||||
)
|
)
|
||||||
_patch_session(monkeypatch, _FakeBedrockControl(exc=bad_key), {})
|
_patch_session(monkeypatch, _FakeBedrockControl(exc=bad_key), {})
|
||||||
out = verify_provider_key("bedrock", fields={"region": "us-east-1"})
|
out = verify_provider_key(
|
||||||
|
"bedrock", fields={"region": "us-east-1", "auth_method": "profile"}
|
||||||
|
)
|
||||||
assert not out["ok"] and "rejected" in out["error"]
|
assert not out["ok"] and "rejected" in out["error"]
|
||||||
|
|||||||
Reference in New Issue
Block a user