feat(providers): add Devin via the official CLI ACP server - #9483
feat(providers): add Devin via the official CLI ACP server#9483abhinav-bhateja wants to merge 1 commit into
Conversation
Devin ships an official ACP server (devin acp) but T3 Code had no driver for it, so Devin Pro subscriptions could not be used from T3 Code. This adds Devin as a seventh built-in provider, off by default and listed last. Everything runs through the local Devin CLI over ACP stdio: version check via devin --version, auth via devin auth status, and model catalog from the session model config option (197 models). Sessions default to the Code (accept-edits) mode with T3 permission prompts, Plan interaction maps to Devin's plan mode, and Full access maps to Bypass Permissions. Includes the ACP adapter, snapshot/status provider, skill discovery, text generation, web and mobile icons (official chain-link mark), settings entries, and docs. Verified against the real CLI (devin 3000.6.7, logged in): ACP initialize, session/new modes and model options, and model discovery through the shared probe runtime (DevinAcpCliProbe 3/3). Unit suites: DevinAcpSupport + DevinProvider 13/13, contracts 79/79, web 52/52; tsgo clean on contracts/server/web/mobile; vp lint clean on touched files. Built with Muse Spark in T3 Code.
| function flattenModelSelectOptions( | ||
| configOption: EffectAcpSchema.SessionConfigOption | undefined, | ||
| ): ReadonlyArray<DevinAcpDiscoveredModel> { | ||
| if (!configOption || configOption.type !== "select") return []; |
There was a problem hiding this comment.
🟠 High Layers/DevinProvider.ts:123
When ACP model discovery is unavailable or flattenModelSelectOptions returns no models, the status check passes discoveredModels: [] to buildDevinProviderSnapshot, which removes the built-in adaptive fallback and can leave the enabled provider with no selectable model. Preserve fallbackModels whenever discovery fails, times out, is skipped, or returns an empty/unsupported catalog.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/DevinProvider.ts around line 123:
When ACP model discovery is unavailable or `flattenModelSelectOptions` returns no models, the status check passes `discoveredModels: []` to `buildDevinProviderSnapshot`, which removes the built-in `adaptive` fallback and can leave the enabled provider with no selectable model. Preserve `fallbackModels` whenever discovery fails, times out, is skipped, or returns an empty/unsupported catalog.
| let frontmatter: DevinSkillFrontmatter | undefined = { cliVisible: true }; | ||
| if (skillInfo.size <= MAX_SKILL_BYTES && skillInfo.size <= input.budget.remainingBytes) { | ||
| const contents = yield* orUndefined(fileSystem.readFileString(skillPath)); | ||
| if (contents !== undefined) { | ||
| input.budget.remainingBytes -= skillInfo.size; | ||
| frontmatter = parseSkillFrontmatter(contents); | ||
| } | ||
| } |
There was a problem hiding this comment.
🟡 Medium Drivers/DevinSkills.ts:162
A SKILL.md that fits MAX_SKILL_BYTES but exceeds remainingBytes is treated as having { cliVisible: true }, so the scan can return skills whose metadata excludes cli and probeDevinSkills reports success after an incomplete scan. Read failures have the same fallback and are silently ignored because orUndefined(fileSystem.readFileString(skillPath)) receives no input.budget; pass the budget, mark the scan exhausted when the file cannot fit, and exclude the skill when its metadata read fails.
- let frontmatter: DevinSkillFrontmatter | undefined = { cliVisible: true };
- if (skillInfo.size <= MAX_SKILL_BYTES && skillInfo.size <= input.budget.remainingBytes) {
- const contents = yield* orUndefined(fileSystem.readFileString(skillPath));
- if (contents !== undefined) {
- input.budget.remainingBytes -= skillInfo.size;
- frontmatter = parseSkillFrontmatter(contents);
- }
+ let frontmatter: DevinSkillFrontmatter | undefined = { cliVisible: true };
+ if (skillInfo.size <= MAX_SKILL_BYTES) {
+ if (skillInfo.size > input.budget.remainingBytes) {
+ input.budget.exhausted = true;
+ return;
+ }
+ const contents = yield* orUndefined(fileSystem.readFileString(skillPath), input.budget);
+ if (contents === undefined) {
+ frontmatter = undefined;
+ } else {
+ input.budget.remainingBytes -= skillInfo.size;
+ frontmatter = parseSkillFrontmatter(contents);
+ }🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Drivers/DevinSkills.ts around lines 162-169:
A `SKILL.md` that fits `MAX_SKILL_BYTES` but exceeds `remainingBytes` is treated as having `{ cliVisible: true }`, so the scan can return skills whose metadata excludes `cli` and `probeDevinSkills` reports success after an incomplete scan. Read failures have the same fallback and are silently ignored because `orUndefined(fileSystem.readFileString(skillPath))` receives no `input.budget`; pass the budget, mark the scan exhausted when the file cannot fit, and exclude the skill when its metadata read fails.
| // Count this prompt immediately so a superseded in-flight prompt | ||
| // resolving from here on does not settle the turn; the matching | ||
| // decrement is the `ensuring` below. | ||
| ctx.promptsInFlight += 1; |
There was a problem hiding this comment.
🟡 Medium Layers/DevinAdapter.ts:843
A second concurrent sendTurn opens a separate turn and emits another turn.started event instead of steering the existing turn. The first call sets ctx.activeTurnId only after awaiting applyRequestedSessionConfiguration, so the second call sees promptsInFlight > 0 but activeTurnId === undefined; reserve the active turn ID before that await.
| ctx.promptsInFlight += 1; | |
| ctx.promptsInFlight += 1; | |
| ctx.activeTurnId = turnId; |
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/DevinAdapter.ts around line 843:
A second concurrent `sendTurn` opens a separate turn and emits another `turn.started` event instead of steering the existing turn. The first call sets `ctx.activeTurnId` only after awaiting `applyRequestedSessionConfiguration`, so the second call sees `promptsInFlight > 0` but `activeTurnId === undefined`; reserve the active turn ID before that await.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: eb630141b5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| ? ({ outcome: "cancelled" } as const) | ||
| : { | ||
| outcome: "selected" as const, | ||
| optionId: acpPermissionOutcome(resolved), |
There was a problem hiding this comment.
Return Devin's advertised permission option ID
In approval-required sessions, accepting or declining a Devin tool request returns synthetic IDs such as allow-once and reject-once rather than an optionId from params.options. ACP defines these IDs as agent-provided unique identifiers (packages/effect-acp/src/_generated/schema.gen.ts:7663-7666), and the repository's protocol fixtures use values such as allow; therefore any Devin request using opaque IDs—or omitting allow_always—will receive an invalid response and the tool turn can fail or remain blocked. Resolve the selected option by its kind, as the Grok adapter does.
Useful? React with 👍 / 👎.
| discoveredModels: Option.getOrElse( | ||
| Option.filter(discoveredModels, (models) => models.length > 0), | ||
| () => [] as const, |
There was a problem hiding this comment.
Preserve fallback models when discovery fails
When ACP discovery times out, errors, or returns no models, this converts the missing result to [], so the completed health refresh replaces the initial adaptive fallback catalog with an empty model list. Mobile builds its picker directly from provider.models, and web model options do likewise, leaving a correctly installed Devin provider with no selectable model after a transient discovery failure. Pass getDevinFallbackModels(devinSettings) when no discovered models are available instead.
AGENTS.md reference: AGENTS.md:L67-L72
Useful? React with 👍 / 👎.
| const modelOption = configOptions.find((option) => option.id === "model"); | ||
| return buildDevinDiscoveredModels(flattenModelSelectOptions(modelOption)); |
There was a problem hiding this comment.
Honor the ACP model option's current value
Model discovery flattens the model select but discards its currentValue, so none of the resulting ServerProviderModel entries is marked isDefault. Both web and mobile then choose the first catalog entry rather than Devin's active default whenever those differ; ACP does not require the current value to be listed first. Propagate modelOption.currentValue and mark the matching model as default.
Useful? React with 👍 / 👎.
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — This PR adds a substantial Devin provider integration with ACP session management, authentication and permission handling, model discovery, skill scanning, UI exposure, and text-generation support across production paths. Its size and runtime scope, along with unresolved concerns around model fallback and permission responses, require human review. Not approved because:
Adjust the Minimum Blocking Severity for this repo — including turning it Off — in Settings. You can add or adjust custom eligibility rules. Learn more. |
|
Note 🤖 GPT-6 Astra (preview) responding on behalf of Theo This was closed as part of an automated cleanup pass. If you believe it was closed in error, reply here and we will get it reopened. Closing this duplicate in favor of the dedicated Devin implementation in #9335. That PR handles advertised permission IDs and rejects overlapping turns. This PR's ACP model config-option and skill-discovery findings are recorded there for follow-up. The retained PR still needs review and live-turn verification. |
Devin ships an official ACP server (\devin acp) but T3 Code has no driver for it, so Devin subscriptions cannot be used from T3 Code.
This adds Devin as a seventh built-in provider, off by default and listed last. Everything runs through the local Devin CLI over ACP stdio. No custom install or OAuth stack is needed (unlike Antigravity): the CLI owns credentials via \devin auth login.
What the user does
How it works
Verified on the real CLI
Linux/Windows CLI \devin 3000.6.7, logged in as Devin Pro:
Not covered here
Built with Muse Spark in T3 Code.
Note
Medium Risk
Large new provider surface (child processes, ACP sessions, filesystem skill scans, permission mapping) alongside contracts changes that affect all clients; behavior is mostly isolated to the Devin adapter path but mis-mapped modes or session lifecycle bugs could affect agent safety and turn completion.
Overview
Adds Devin as a seventh built-in provider (opt-in, sorted last), wired to the local Devin CLI over
devin acp—no custom OAuth or install stack like Antigravity.Contracts & settings: New
DevinSettings(enabledoff by default,binaryPath,customModels), server settings/patch wiring, default chat modeladaptive, and display-name defaults.Server:
DevinDriverregisters the driver with health checks (devin --version,devin auth status), ACP model discovery,devin updatemaintenance, workspace Agent Skills scanning (composer$skill→ Devin/skill), andDevinTextGenerationfor titles/commits/PRs in ask mode.DevinAdaptermaps T3 runtime/interaction modes to Devin session modes (plan / bypass / accept-edits), streams ACP events (including steer-in-flight turns), handles permissions, native images, session resume, and MCP hookup.Clients & docs: Devin icons and provider metadata on web and mobile; settings/model picker/session logic updated. User and contributor docs list Devin install (
devin auth login) and note Devin behaves like OpenCode/Antigravity for Auto permission mode (falls back to supervised approvals).Reviewed by Cursor Bugbot for commit eb63014. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add Devin provider via official CLI ACP server
devin acpcommandDevinSettings; users set a binary path and rundevin loginto enable itadaptivemodel when discovery fails or no model is selected$skillmentions to/skillinvocations in promptsbuiltInDrivers.tsnow exportsDevinDriverandDevinDriverEnv; any code constructing the built-in driver registry must supply Devin driver environment services or compilation fails📊 Macroscope summarized eb63014. 23 files reviewed, 7 issues evaluated, 1 issue filtered, 3 comments posted
🗂️ Filtered Issues
docs/user/permission-modes.md — 0 comments posted, 1 evaluated, 1 filtered
Automode falls back toSupervised(which promises approval before file changes), but the Devin adapter maps every non-full-accessruntime mode—includingAuto—to Devin'saccept-editsmode. That mode auto-approves workspace edits, so users selecting Auto can have changes applied without the prompts the documentation promises. [ Out of scope (triage) ]