Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
06ec553
Merge pull request #3678 from lidge-jun/codex/promote-main-243-01a07240
lidge-jun Sep 5, 2026
116c2ac
Merge commit '44ea9576e27c6be8be7f13a86e32bb349368c54d' into codex/re…
invalid-email-address Sep 6, 2026
07b48da
Merge pull request #3785 from lidge-jun/codex/release-244-main-07c0
lidge-jun Sep 6, 2026
bcdf559
chore(release): promote validated 2.45.0 to main [skip ci]
invalid-email-address Sep 6, 2026
b0900e5
chore(release): promote 2.45.0 to main (#3813)
lidge-jun Sep 6, 2026
3970601
chore(release): prepare 2.46.0 stable promotion
invalid-email-address Sep 7, 2026
bba6322
Merge pull request #3851 from lidge-jun/codex/release-246-main
lidge-jun Sep 7, 2026
3d53e5f
release: prepare 2.47.0 from audited regression candidate
invalid-email-address Sep 7, 2026
eda8754
Merge commit '48ab3e1e66cfa6e0c873de2fafa4540ac61d6c7d' into codex/re…
invalid-email-address Sep 7, 2026
f9e3515
Merge commit '57252193b' into codex/release-247-main
invalid-email-address Sep 7, 2026
6f71931
release: promote 2.47.0 to main (#3929)
lidge-jun Sep 7, 2026
9a60256
Merge commit 'd0737cff3' into codex/release-247-main-final
invalid-email-address Sep 7, 2026
9e9b1d3
Merge commit 'f48c322c0' into codex/release-247-main-final
invalid-email-address Sep 7, 2026
947bae9
Merge commit '0d7652ad1' into codex/release-247-main-final
invalid-email-address Sep 7, 2026
f7f890f
release: apply final roster correction to main (#3933)
lidge-jun Sep 7, 2026
544ebee
release: promote 2.48.0 to main
invalid-email-address Sep 8, 2026
d24ff57
release: set main channel version 2.48.0
invalid-email-address Sep 8, 2026
9a27e86
Merge pull request #4011 from lidge-jun/codex/release-248-main
lidge-jun Sep 8, 2026
62849df
release: promote verified 2.49.0 product tree to main
lidge-jun Sep 9, 2026
2f3f736
Merge pull request #4117 from lidge-jun/codex/release-249-main-01a08498
lidge-jun Sep 9, 2026
3a3de88
release: promote verified 2.50.0 product tree to main
lidge-jun Sep 10, 2026
2d4d7a2
Merge pull request #4195 from lidge-jun/codex/release-250-main-01a08a81
lidge-jun Sep 10, 2026
cf456e8
release: promote verified 2.51.0 product tree to main
lidge-jun Sep 11, 2026
c155cc7
Merge pull request #4271 from lidge-jun/codex/release-251-main
lidge-jun Sep 11, 2026
95c4875
release: promote verified 2.52.0 product tree to main
lidge-jun Sep 12, 2026
4d37c35
Merge pull request #4407 from lidge-jun/codex/release-2520-main
lidge-jun Sep 12, 2026
641b05a
release: promote verified 2.53.0 product tree to main
lidge-jun Sep 13, 2026
aa05b3e
Merge pull request #4507 from lidge-jun/codex/release-2530-main
lidge-jun Sep 13, 2026
8e532c5
release: promote verified 2.54.0 product tree to main
lidge-jun Sep 13, 2026
9f7397e
Merge pull request #4540 from lidge-jun/codex/release-2540-main
lidge-jun Sep 13, 2026
e3cca96
fix(oauth): keep Devin migration account-bound
luvs01 Sep 14, 2026
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
45 changes: 33 additions & 12 deletions src/providers/devin-provider-merge-migration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,33 +195,54 @@ const DEFAULT_DEPS: DevinProviderMergeStartupDeps = {
* the snapshot is taken strictly before the save, and a backup failure throws
* rather than writing without a rollback point.
*
* The auth half is deliberately detached. `startServer` is synchronous — an
* `await` in the boot window would suspend the composition root — and
* Both destination slots are inspected before either account-bound file is
* changed. The auth write itself is deliberately detached. `startServer` is
* synchronous — an `await` in the boot window would suspend the composition root — and
* `mutateStore` is async-only, so the rekey is fired after its snapshot and
* its outcome is logged when it lands. That is safe here: the credential is
* valid under either slot name while the `devin-cli` alias exists, a conflict
* refuses by design, and a failed rekey simply retries on the next boot.
* its outcome is logged when it lands. A late concurrent conflict refuses by
* design, and a failed rekey simply retries on the next boot.
*/
export function runDevinProviderMergeStartupMigration(
config: OcxConfig,
deps: DevinProviderMergeStartupDeps = DEFAULT_DEPS,
): OcxConfig {
const projection = deps.project(config);
// Warnings are emitted even on a no-op: the collision case IS the warning.
const hasSourceConfig = config.providers?.[FROM_ID] !== undefined;
const hasSourceAuth = deps.hasAuthSlot(FROM_ID);
const hasDestinationAuth = deps.hasAuthSlot(TO_ID);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Make the destination check atomic with the rekey

When another process or an already queued OAuth mutation creates auth.json["devin"] after this synchronous peek but before the detached rekeyProviderCredentials acquires the auth-store lock, hasDestinationAuth is stale and the config is saved under devin; the rekey then returns conflict and leaves both credential slots. Because createDevinAdapter resolves credentials using the configured provider ID verbatim, subsequent requests use the newly inserted devin account, recreating the cross-account binding this change is intended to prevent. Reserve or move the destination under the auth-store lock before committing the config, or defer the config commit until the locked rekey succeeds.

AGENTS.md reference: AGENTS.md:L366-L372

Useful? React with 👍 / 👎.


// A configured provider and its credentials are one account-bound unit. Do
// not move either half if the config projection refused, or if the target
// credential slot could belong to another account.
if (hasSourceConfig && (!projection.changed || hasDestinationAuth)) {
// Projection warnings still matter on a no-op: a config collision is the warning.
for (const warning of projection.warnings) console.warn(`[devin-provider-merge] ${warning}`);
if (projection.changed && hasDestinationAuth) {
console.warn(
`[devin-provider-merge] auth.json already has a "${TO_ID}" credential slot; `
+ `provider "${FROM_ID}" and both credential slots were left untouched. Remove the `
+ "unused destination credential manually, then restart.",
);
}
return config;
}

for (const warning of projection.warnings) console.warn(`[devin-provider-merge] ${warning}`);

let result = config;
if (projection.changed) {
// Snapshot both account-bound files before changing either one.
if (hasSourceAuth) deps.backupAuth();
deps.backupConfig();
deps.save(projection.config);
result = projection.config;
}

// The auth rekey runs even when the config half refused or had nothing to
// do: a `devin-cli` credential slot is orphaned state regardless of whether
// a provider row still points at it, and the conflict check inside the
// rekey is the same refuse-on-occupied rule the config half applies.
if (!deps.hasAuthSlot(FROM_ID)) return result;
deps.backupAuth();
// With no legacy config row, a `devin-cli` credential slot is orphaned and
// can still be rekeyed under the helper's refuse-on-occupied rule. A refused
// config migration returned above so its account-bound slot stays put.
if (!hasSourceAuth) return result;
if (!projection.changed) deps.backupAuth();
void deps.rekey(FROM_ID, TO_ID).then(outcome => {
if (outcome === "conflict") {
console.warn(
Expand Down
2 changes: 2 additions & 0 deletions structure/providers/xai-grok.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,5 @@ Pool quota producers and account commands follow the [bounded raw-observation co
Account quota surfaces use [safe probe diagnostics](../transports/inventory.md#account-quota-failure-diagnostics) separately from quota validity, credential health and routing authority.

Live sideband admission and its bounded upstream handshake follow the [runtime contract](../runtime.md#live-sideband-handshake); the ordinary Responses WebSocket exchange remains separate.

Shared startup provider-id migration preserves the account binding between configuration and OAuth credentials; see the [runtime contract](../runtime.md).
2 changes: 2 additions & 0 deletions structure/runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,8 @@ The lightweight top-level CLI help counts Cline CLI among the fifteen registered

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 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.

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
2 changes: 2 additions & 0 deletions structure/subagents.md
Original file line number Diff line number Diff line change
Expand Up @@ -371,3 +371,5 @@ Exact [model input declarations](config.md#explicit-per-model-capability-declara
Provider-scoped approval reviewer settings are projected by the [catalog owner](catalog.md#provider-scoped-approval-reviewer); this surface retains its existing routing, transport and account-selection behavior.

Renamed fixed-key providers receive [missing reasoning metadata](catalog.md#renamed-destination-reasoning-metadata) during derivation; explicit per-model entries and provider defaults retain precedence.

Startup provider-id migration preserves the account binding between configuration and OAuth credentials; see the [runtime contract](runtime.md).
2 changes: 2 additions & 0 deletions structure/transports/inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,5 @@ The [explicit model-capability contract](../config.md#explicit-per-model-capabil
Provider-scoped approval reviewer settings are projected by the [catalog owner](../catalog.md#provider-scoped-approval-reviewer); this surface retains its existing routing, transport and account-selection behavior.

Renamed fixed-key providers receive [missing reasoning metadata](../catalog.md#renamed-destination-reasoning-metadata) during derivation; explicit per-model entries and provider defaults retain precedence.

Startup provider-id migration preserves the account binding between configuration and OAuth credentials; see the [runtime contract](../runtime.md).
36 changes: 32 additions & 4 deletions tests/providers/devin-provider-merge-migration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,15 +129,15 @@ describe("devin provider merge startup runner", () => {
backupConfig: () => { order.push("backupConfig"); },
backupAuth: () => { order.push("backupAuth"); },
save: () => { order.push("save"); },
hasAuthSlot: () => opts.hasAuthSlot ?? false,
hasAuthSlot: (provider: string) => provider === "devin-cli" && (opts.hasAuthSlot ?? false),
rekey: async (from: string, to: string) => { order.push(`rekey:${from}->${to}`); return opts.rekey ? opts.rekey() : "moved" as const; },
};
}

test("snapshots config strictly before saving, and rekeys the auth slot", async () => {
const order: string[] = [];
const result = runDevinProviderMergeStartupMigration(migratableConfig(), depsWith(order, { hasAuthSlot: true }));
expect(order.slice(0, 2)).toEqual(["backupConfig", "save"]);
expect(order.slice(0, 3)).toEqual(["backupAuth", "backupConfig", "save"]);
expect(order).toContain("backupAuth");
expect(order).toContain("rekey:devin-cli->devin");
expect(result.providers!['devin']).toBeDefined();
Expand Down Expand Up @@ -178,13 +178,41 @@ describe("devin provider merge startup runner", () => {
expect(warnings.join(" ")).toContain("[devin-provider-merge]");
});

test("a rekey conflict warns rather than throwing out of startup", async () => {
test("an auth destination collision refuses both halves of the migration", () => {
const order: string[] = [];
const warnings: string[] = [];
const originalWarn = console.warn;
console.warn = (...args: unknown[]) => { warnings.push(args.map(String).join(" ")); };
try {
runDevinProviderMergeStartupMigration(migratableConfig(), depsWith(order, { hasAuthSlot: true, rekey: async () => "conflict" }));
const deps = depsWith(order, { hasAuthSlot: true });
deps.hasAuthSlot = provider => provider === "devin-cli" || provider === "devin";
const config = migratableConfig();
const result = runDevinProviderMergeStartupMigration(config, deps);
expect(result).toBe(config);
} finally {
console.warn = originalWarn;
}
expect(order).toEqual([]);
expect(warnings.join(" ")).toContain('auth.json already has a "devin" credential slot');
});

test("a config collision never independently rekeys credentials", () => {
const order: string[] = [];
const config = migratableConfig();
config.providers!["devin"] = { adapter: "devin" } as never;
runDevinProviderMergeStartupMigration(config, depsWith(order, { hasAuthSlot: true }));
expect(order).toEqual([]);
});

test("a late rekey conflict warns rather than throwing out of startup", async () => {
const order: string[] = [];
const warnings: string[] = [];
const originalWarn = console.warn;
console.warn = (...args: unknown[]) => { warnings.push(args.map(String).join(" ")); };
try {
const deps = depsWith(order, { hasAuthSlot: true, rekey: async () => "conflict" });
deps.hasAuthSlot = provider => provider === "devin-cli";
runDevinProviderMergeStartupMigration(migratableConfig(), deps);
// The detached promise needs a real tick, not one microtask.
await new Promise(resolve => setTimeout(resolve, 0));
} finally {
Expand Down
Loading