Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/adapters/devin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -612,7 +612,7 @@ export function createDevinAdapter(
// The signed-in account's tenant decides the host, not the static registry
// entry: an EU or FedStart account that used provider.baseUrl would send
// every RPC to the US server it is not provisioned on.
const host = resolveDevinApiServer(provider.baseUrl, credentialProviderId);
const host = resolveDevinApiServer(provider.baseUrl, credentialProviderId, apiKey);
// One catalog read per turn serves model-UID resolution, the input
// ceiling, and the chat pre-flight inside streamChatEvents. Failures are
// not cached, so a second read would only pay another fetch timeout on
Expand Down
11 changes: 8 additions & 3 deletions src/oauth/devin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ function devinAliasCredentialSlots(providerId: string): string[] {
* resort; both are re-validated because neither is trusted more than the
* network value.
*/
export function resolveDevinApiServer(configuredBaseUrl?: string, providerId = "devin"): string {
export function resolveDevinApiServer(configuredBaseUrl?: string, providerId = "devin", apiKey?: string): string {
// Provider-scoped, keyed by the configured provider id verbatim and consulted
// FIRST. `devin-cli` is a deprecated alias for `devin`, but an unmigrated
// config row still owns its old credential slot until the startup migration
Expand All @@ -72,7 +72,10 @@ export function resolveDevinApiServer(configuredBaseUrl?: string, providerId = "
// directions closes that window: "devin" finds the not-yet-rekeyed
// "devin-cli" credential, and a lingering "devin-cli" row finds a credential
// already rekeyed to "devin". Every candidate passes the same allowlist — an
// alias slot is not trusted more than the literal one.
// alias slot is not trusted more than the literal one. The candidate must
// also own the key this request will transmit: provider-configured and
// forwarded keys are resolved outside the credential store, and borrowing a
// host from a different key would cross an account or regional boundary.
// Only when this id owns no credential at all. A present credential whose
// apiBaseUrl is missing or off-allowlist is a different situation: the rekey
// refuses an occupied destination slot, so both ids can hold credentials that
Expand All @@ -84,7 +87,9 @@ export function resolveDevinApiServer(configuredBaseUrl?: string, providerId = "
// closed.
if (literalCredential === null || literalCredential === undefined) {
for (const slot of devinAliasCredentialSlots(providerId)) {
const host = validateDevinApiBaseUrl(getCredential(slot)?.apiBaseUrl);
const credential = getCredential(slot);
if (!apiKey || credential?.access !== apiKey) continue;
const host = validateDevinApiBaseUrl(credential.apiBaseUrl);
if (host !== undefined) return host;
}
}
Expand Down
2 changes: 1 addition & 1 deletion structure/providers-and-adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ the [bounded ingestion contract](transports/inventory.md#bounded-response-ingest
| `src/adapters/declaration-carrier.ts`, `src/adapters/input-media-guard.ts` | Default-deny allowlists for constraints the normalized request carries but a wire may not be able to express: `tools[*].allowed_callers`, which fences a tool off from callers, and inline document bytes. Both are refused with a 400 at the single guard every registered adapter passes through, rather than left to each adapter, because an adapter that never learned about the carrier rebuilds without it and answers normally. `allowed_callers` reaches the `anthropic` wire; document bytes reach `anthropic`, `openai-chat` and `google`; the `openai-responses` wire is exempt from the whole guard because it forwards the original body. Adding an `AdapterWire` member makes the omission visible in these lists instead of at a customer's upstream. The unrestricted `["direct"]` caller default is not a restriction. |
| `src/adapters/azure.ts` | Azure OpenAI bridge. |
| `src/adapters/cursor.ts`, `src/adapters/cursor/` | Cursor protobuf transport: discovery, request builder, event decoding, MCP, thread continuity, native-exec policy. |
| `src/adapters/devin.ts`, `src/adapters/devin/cloud-direct/` | Devin runTurn transport over Cognition Connect-RPC. `GetChatMessage` uses the Responses provider executor and shared physical-send budget; catalog and JWT support RPCs remain outside inference-send accounting. |
| `src/adapters/devin.ts`, `src/adapters/devin/cloud-direct/` | Devin runTurn transport over Cognition Connect-RPC. `GetChatMessage` uses the Responses provider executor and shared physical-send budget; catalog and JWT support RPCs remain outside inference-send accounting. Tenant-host fallback across the deprecated provider alias is admitted only when the alias credential owns the transmitted key. |
| `src/adapters/kiro.ts` and `src/adapters/kiro/` | Kiro event/tool/thinking/truncation/retry handling. The original path is a facade over leaves for wire identity, reasoning, conversation state, token estimation, payload assembly, streaming, and the adapter. |
| `src/adapters/mimo-free.ts` | Mimo Free transport (client identity + JWT). |
| `src/adapters/image.ts`, `src/adapters/anthropic-image-guard.ts`, `src/adapters/anthropic-image-normalize.ts`, `src/adapters/anthropic-image-codec.ts` | Image conversion for adapter ingress and Anthropic-specific normalization/limits. An image's ladder position is pinned to its own identity (content hash + media type), so appending a newer image cannot re-encode older ones and bust Anthropic's prompt prefix cache (#4532). |
Expand Down
2 changes: 1 addition & 1 deletion structure/runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,7 @@ following a final symlink, so an exchange during a mutation cannot redirect the
Config JSON preserves the boolean; only literal true activates the role-changing transform.
The lightweight top-level CLI help counts Cline CLI among the fifteen registered export clients; registry parity remains covered by the client help and integration tests.

Devin CLI credential path composition in `src/oauth/devin/cli-import.ts` follows the selected platform: Windows uses Win32 APPDATA paths, other platforms use POSIX XDG-data paths. The explicit absolute override remains verbatim; credential parsing and login behavior are unchanged. The `src/providers/devin-provider-merge-migration.ts` startup migration treats the legacy provider row and its OAuth slot as one account-bound unit: an occupied destination or a refused config projection leaves both unchanged, and both backups complete before either file changes.
Devin CLI credential path composition in `src/oauth/devin/cli-import.ts` follows the selected platform: Windows uses Win32 APPDATA paths, other platforms use POSIX XDG-data paths. The explicit absolute override remains verbatim; credential parsing and login behavior are unchanged. The `src/providers/devin-provider-merge-migration.ts` startup migration treats the legacy provider row and its OAuth slot as one account-bound unit: an occupied destination or a refused config projection leaves both unchanged, and both backups complete before either file changes. During a detached rekey window, the adapter may use the alias slot's tenant host only when that slot owns the exact key being transmitted, so separately configured or forwarded credentials cannot borrow another account's destination.

Native Chat applies qualifying effort ceilings independently of model pins; pin selection precedes the cap and only pins or cap rewrites enter wire mapping. The [catalog effort contract](catalog.md#ultra-reasoning-level) records the V1/compaction exemptions and caller-preservation boundary.
Pool quota producers and account commands follow the [bounded raw-observation contract](providers/openai-tiers.md#bounded-pool-quota-observations), separate from the latest display snapshot and capacity estimates.
Expand Down
4 changes: 4 additions & 0 deletions tests/adapters/openai/openai-chat-dangling-toolcalls.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ const provider: OcxProviderConfig = {
baseUrl: "https://example.test/v1",
apiKey: "sk-test",
authMode: "key",
// The wire role folds to `system` unless a destination is recorded as accepting
// `developer`; this suite is about tool-result repair ordering, so it declares the
// destination rather than asserting the default.
foldDeveloperRoleToSystem: false,
};

interface ChatMsg {
Expand Down
21 changes: 18 additions & 3 deletions tests/providers/devin-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -500,10 +500,10 @@ describe("devin adapter api-server host resolution (#4503)", () => {

// Drive one real runTurn. The stubbed 500 ends the turn in an upstream error
// only after every outbound URL has been recorded.
async function runOneTurn(apiKey: string): Promise<AdapterEvent[]> {
async function runOneTurn(apiKey: string, providerId = "devin"): Promise<AdapterEvent[]> {
const adapter = createDevinAdapter(
{ adapter: "devin", baseUrl: CONFIGURED_BASE_URL, apiKey },
{ providerId: "devin" },
{ providerId },
);
const parsed: OcxParsedRequest = {
modelId: "swe-2-high",
Expand Down Expand Up @@ -534,7 +534,7 @@ describe("devin adapter api-server host resolution (#4503)", () => {
apiBaseUrl: EU_TENANT_HOST,
});

const events = await runOneTurn("ocx-test-alias-slot-key");
const events = await runOneTurn("devin-cli-session");

expectDispatchedTo(EU_TENANT_HOST);
expect(seenUrls.some((url) => url.startsWith(DEVIN_DEFAULT_API_SERVER))).toBe(false);
Expand All @@ -544,6 +544,21 @@ describe("devin adapter api-server host resolution (#4503)", () => {
expect(events.some((event) => event.type === "error")).toBe(true);
});

test("an independently configured key does not borrow an alias credential's tenant host", async () => {
await saveCredential("devin", {
access: "different-account-session",
refresh: "different-account-session",
expires: Number.MAX_SAFE_INTEGER,
source: "local-cli",
apiBaseUrl: EU_TENANT_HOST,
});

await runOneTurn("configured-provider-key", "devin-cli");

expectDispatchedTo(CONFIGURED_BASE_URL);
expect(seenUrls.some((url) => url.startsWith(EU_TENANT_HOST))).toBe(false);
});

test("a usable literal devin slot still wins over the aliased devin-cli slot", async () => {
await saveCredential("devin", {
access: "devin-session",
Expand Down
4 changes: 2 additions & 2 deletions tests/providers/devin-login.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,14 +259,14 @@ describe("devin tenant selection is provider-scoped", () => {
// tenant host sitting on "devin-cli"; otherwise the key is sent to the US
// default and Cognition answers permission_denied.
await seedSlot("devin-cli", EU_HOST);
expect(resolveDevinApiServer(undefined, "devin")).toBe(EU_HOST);
expect(resolveDevinApiServer(undefined, "devin", KEY)).toBe(EU_HOST);
});

test("the signed-in alias tenant wins over a configured baseUrl", async () => {
// RegisterUser recorded the tenant on the credential. A leftover US
// baseUrl on the rewritten config row must not override that account.
await seedSlot("devin-cli", EU_HOST);
expect(resolveDevinApiServer(US_HOST, "devin")).toBe(EU_HOST);
expect(resolveDevinApiServer(US_HOST, "devin", KEY)).toBe(EU_HOST);
});

test("the literal slot wins when both alias ids hold a tenant", async () => {
Expand Down
4 changes: 4 additions & 0 deletions tests/responses/chat-inline-document-bytes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ const chatProvider: OcxProviderConfig = {
adapter: "openai-chat",
baseUrl: "https://gateway.example.internal/v1",
apiKey: "k",
// The wire role folds to `system` unless a destination is recorded as accepting
// `developer`; the document test asserts the role a turn keeps, so it declares the
// destination rather than asserting the default.
foldDeveloperRoleToSystem: false,
};
const anthropicProvider = {
adapter: "anthropic",
Expand Down
Loading