Skip to content

feat(devin): create-time permission-mode picker in the composer - #7287

Closed
dhruv0811 wants to merge 20 commits into
mainfrom
dhruvgupta/devin-permission-mode
Closed

dhruv0811 wants to merge 20 commits into
mainfrom
dhruvgupta/devin-permission-mode

Conversation

@dhruv0811

Copy link
Copy Markdown
Member

Related issue

N/A — feature stacked on #7254 (devin-native harness). No separate issue.

Summary

Stacked on #7254. Adds a create-time permission-mode picker for Devin in the composer's permissions hand-menu — previously Devin's --permission-mode was reachable only via omnigent devin on the CLI.

  • Devin uses the same --permission-mode <mode> launch flag as Claude Code, so the pick rides terminal_launch_args exactly like Claude's. But Devin has its own vocabulary (normal / accept-edits / smart / dangerous, verified against devin 3000.10.21) and no running-session switch.
  • To avoid entangling Claude's mid-chat permission machinery, this is an isolated devinPermissionMode capability mirroring Codex's separate approvalMode pattern rather than reusing permissionMode.
  • normal (default) sends no flag, so Devin keeps its own configured default.

No server change: terminal_launch_args validation is shape-only and the runner already appends passthrough to the Devin launch, so the picked flag reaches the CLI as-is.

ELI5: Devin already understood --permission-mode; we just added the dropdown that sends it, with Devin's own list of modes.

Test Plan

  • web: vitest run src/shell/NewChatDialog.test.tsx src/lib/nativeCodingAgents.test.ts src/pages/ChatPage.capabilities.test.ts — 443 pass, incl. two new tests asserting a picked mode emits ["--permission-mode", <mode>] and the default emits nothing.
  • tsc --noEmit clean; oxlint clean; prettier clean.
  • The parametrized "every native agent" menu test now exercises Devin's config submenu.

Demo

  • Non-visual evidence provided below or in Test Plan

UI change, but the composer menu could not be captured headlessly in this environment (no live browser). By-eye verification of the rendered permission chip/menu is pending a manual check: start omnigent devin on this branch, open the composer permissions menu, and confirm the Normal/Accept edits/Smart/Dangerous options appear and a non-default pick starts Devin with that --permission-mode.

Type of change

  • Bug fix
  • Feature
  • UI / frontend change
  • Refactor / chore
  • Docs
  • Test / CI
  • Breaking change

Test coverage

  • Unit tests added / updated
  • Integration tests added / updated
  • Manual verification completed

Coverage notes

Manual verification is limited to CLI + automated tests; the composer menu's visual rendering is unverified by eye in this environment (see Demo).

Changelog

Devin sessions can pick a permission mode (Normal / Accept edits / Smart / Dangerous) from the composer.

This pull request and its description were written by Isaac.

dhruv0811 and others added 17 commits September 12, 2026 07:21
`omnigent devin` wraps the resident Devin CLI TUI in a runner-owned tmux pane and
mirrors it into an Omnigent conversation. It sits ALONGSIDE Devin's existing ACP
harness rather than replacing it:

* `devin-native` — this wrap. Omnigent policy enforcement, approval cards,
  model + effort, resume, interrupt, cost tracking.
* `devin-acp` — the ACP path (`omnigent/inner/devin`), unchanged in behavior and
  still the only one that surfaces Devin's sub-agents as child sessions. It moves
  from the row id `devin` to `devin-acp` so the bare vendor spelling can
  canonicalize to the native wrap, the way `opencode` resolves to
  `opencode-native`.

Devin exposes a Claude-Code-shaped lifecycle-hook system carrying two ids the
other native harnesses have to reconstruct — `prompt_id` (used directly as the
Omnigent turn id) and `tool_use_id` (pairs a tool call with its output) — plus
`last_assistant_message` on `Stop`. So the mirror is hook-driven rather than
screen-scraped, and the forwarder is a fraction of the size of the
transcript-parsing ones.

Verified live against devin 3000.10.21:

* A `PreToolUse` hook returning `hookSpecificOutput.permissionDecision: "deny"`
  blocks the tool even under `--permission-mode bypass`, and the reason reaches
  the model — so the shared `native_policy_hook` seam drives Devin unmodified
  (elicitation=HOOK, as claude-native). `PermissionRequest` accepts
  `{"decision": "approve"}`.
* Interrupt needs Escape *twice* ("esc twice to interrupt"); one only clears the
  composer draft.
* The composer stays writable mid-turn, so a queued message steers the running
  turn (live_queue/steering).
* Effort is a model-variant suffix, not a flag, so a (model, effort) pair is
  composed into one id at launch and validated against `devin models list`.

Hooks are registered through a session-scoped `--config` file merged from the
user's own config, so the user's settings survive and nothing is written into
their repository.

Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: Dhruv Gupta <dhruv0811@gmail.com>
…ist pinned

Three CI-surfaced fixes on top of the devin-native harness:

* `pickerEffortOptions` returned `[]` for devin-native, so the New Chat composer
  rendered no Effort section at all. Devin has no `--effort` flag — effort is a
  model-variant suffix Omnigent composes at launch — so without the ladder there
  was no way to choose effort when starting a chat, which is the whole point of
  carrying it as a separate axis. Devin now shares the Anthropic rung set, which
  is exactly its ladder.

* Dropped `fullySupported` from the Devin row. That flag pins the picker's
  primary list to Claude Code + Codex and `nativeCodingAgents.test.ts` asserts
  exactly those two, so promoting a brand-new harness there is a maintainer's
  product call rather than a side effect of adding it. Devin folds into "More"
  with the other nine natives; flipping it back is one line.

* `configured_harness_map()` gained the three devin readiness spellings, so the
  hello-frame coverage test lists them.

Adds `tests/e2e_ui/chat/test_devin_native_picker.py`, which the E2E UI judge
asked for: it stubs the host catalog and asserts Devin is offered, that its model
list comes from the devin-native probe (every other harness's stub is empty, so a
wrong catalog shows nothing rather than passing by luck), and that the Effort
ladder renders — the regression the first bullet fixes. Not run locally: the
e2e_ui rig's runner will not come online here, and the existing sibling
`test_harness_support_level_split` fails identically at the same fixture.

Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: Dhruv Gupta <dhruv0811@gmail.com>
Like hermes-native, devin-native is a terminal-first TUI launched via
`omni devin` (tmux pane + bridge dir) rather than
`omnigent run --harness devin-native`, AND it wraps the own-auth `devin` CLI —
so its spawn env carries no gateway/profile probe vars for that matrix to drive.
Devin's ACP row is already excluded as an ACP_CLI_HARNESSES member.

Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: Dhruv Gupta <dhruv0811@gmail.com>
The first version asserted Devin sits behind the Other submenu and that Pi is not
in the primary list — i.e. it pinned the `fullySupported` product decision, which
is not what the test is for. It also failed in CI: the row was not found where the
assertion insisted it must be, and the e2e_ui rig will not boot locally (the
existing sibling test_harness_support_level_split fails at the same fixture), so
the placement could not be checked before pushing.

`_reveal_devin` now looks in the primary list and expands Other only if it has
to, so the test fails on a Devin regression rather than on a placement change.
The substance is unchanged: Devin's model list comes from the devin-native probe
(every other harness's stub is empty, so a wrong catalog shows nothing rather
than passing by luck), and the Effort ladder renders.

Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: Dhruv Gupta <dhruv0811@gmail.com>
test_native_seed_ids_are_byte_stable pins every native built-in's
name-derived agent id, because a drifting id orphans persisted
conversation.agent_id rows on redeploy. devin-native-ui adds one entry.

Verified this is an addition, not a drift: recomputing builtin_agent_id over
NATIVE_CODING_AGENTS leaves all eleven existing ids byte-identical, with
devin-native-ui as the only new key. So no migration is owed and no session is
orphaned — which is exactly the distinction the table's comment asks for before
touching it.

Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: Dhruv Gupta <dhruv0811@gmail.com>
…lity

The devin-native row declares only `devinMode`, but two render gates keyed off
`modelPicker`/`permissionMode`/codex only: `selectedAgentHasKnobs` (which wraps
the whole config dropdown) and the models-section gate. So selecting Devin in
the New Chat picker showed neither a model list nor an effort ladder.
`pickerModelOptions` and `pickerEffortOptions` already resolve devin-native;
only the two gates missed it. OR `supportsDevinMode` into both.

Adds a NewChatDialog vitest that drives the Devin agent through the picker and
asserts its model families + effort rungs render. It fails with the exact
e2e_ui signature (models section absent) without the gate fix.

Co-authored-by: Isaac <no-reply@databricks.com>
Resolves the composer refactor (#7243, #7260) against the devin-native picker:
- pickerEffortOptions: keep Devin's Anthropic-ladder branch, adopt main's
  normalizeEffortLabel rename.
- models section: main moved it into <ComposerConfigSections>; thread
  supportsDevinMode into the new models gate so Devin still renders its model
  list (selectedAgentHasKnobs auto-merged with the same flag).
- ChatPage effectiveModel: keep main's sessionModelSeeded branch, keep devin in
  the vendor-owns-model group.

Co-authored-by: Isaac <no-reply@databricks.com>
…elper

The picker e2e never opened Devin's config submenu — it only revealed the row,
so `new-chat-landing-agent-models` (which lives in that submenu) was never
visible and the test failed regardless of the gate fix. The vitest passed
because it opens the submenu; the e2e drove the UI wrong.

Rewrite it on the shared `_open_entry_models` helper (same navigation the
passing Pi/Codex picker tests use), which clicks the `agent-config-*` Edit
entry. That entry exists only when `selectedAgentHasKnobs` honours `devinMode`,
so the corrected test still fails without the gate fix and passes with it.

Co-authored-by: Isaac <no-reply@databricks.com>
…tore

Devin persists each run_subagent delegate's full transcript as its own chain in
the parent session's message_nodes forest (sessions.db) — not in the hook stream
or the ATIF export. This is the direct analogue of claude-native reading Claude
Code's per-subagent transcript files; Devin keeps the same data in SQLite.

Adds omnigent/harnesses/devin_native/subagents.py:
- reconstruct_transcript_nodes: task-keyed longest leaf→root walk that recovers
  the executed chain and sidesteps the forest's compaction/streaming snapshots
  (a leaf has one ancestor path, so no snapshot guessing);
- transcript_items: converts a chain to the forwarder's own conversation-item
  shapes (user/assistant messages, function_call + paired output, reasoning);
- parsers for the spawned/completed agent_id (which ride only free text) and a
  read-only, fail-soft message_nodes reader.

Pinned against a trimmed real capture (devin 3000.10.21, two parallel
sub-agents) in tests/data/devin_subagent_nodes.json; 16 tests. Not yet wired
into the forwarder — that lands next.

Co-authored-by: Isaac <no-reply@databricks.com>
…l transcripts

Wires the sub-agent transcript reconstruction into the live path so Devin's
run_subagent delegates appear as child sessions in the Agents rail, each
carrying its complete internal transcript (prompt -> tool calls + outputs ->
final report) — parity with claude/codex/antigravity native sub-agents.

Forwarder (harnesses/devin_native/forwarder.py):
- record each run_subagent spawn (agent_id parsed from the tool result, task +
  title from the input), persisted in the forward state;
- on each turn-end, read Devin's session store, and for every newly-completed
  sub-agent reconstruct its chain and post it into a minted child session, then
  close the child idle. Best-effort per sub-agent, marked done to avoid re-posts.

Server (mirrors the codex/antigravity sub-agent-start family):
- external_devin_subagent_start event type + _persist_external_devin_subagent_start,
  keyed idempotently on Devin's agent_id, stamping the devin-native-ui-subagent
  wrapper + title/tool_use_id labels;
- _is_devin_native_subagent + _devin_subagent_display_tool drive the rail label
  (run_subagent title, agent_id as session_name);
- devin joins _ALLOWED_EVENT_TYPES, the parse_item_data bypass set, and the
  terminal-status-forward exclusion (a mirror has no runner work entry).

Capabilities + web: subagents=True + subagent_wrapper_label on the devin-native
row; subagentWrapperLabel on the web row so the child resolves the Devin icon.

Tests: server integration (mint / idempotency / missing-id 400) and forwarder
flow (spawn recorded, completed -> mirrored child with full transcript, not
mirrored until completion).

Co-authored-by: Isaac <no-reply@databricks.com>
…t launch

Two gaps kept the in-chat (in-conversation composer) model/effort controls from
working for devin-native, even though the new-session composer had them:

1. UI gate: supportsEffortControl covered claude/codex/pi but not devin, so the
   in-chat Effort dial never rendered for a Devin session (the model picker did,
   via modelPickerKind). Add devin-native; effortLevelsForConv already returns
   the Anthropic ladder for it.

2. Switching didn't apply: the executor typed /model with config.model alone
   (the bare family), while reasoning_effort rode config.extra and was ignored —
   so an in-chat effort change (or effort-on-a-model change) never reached the
   pane. DevinNativeExecutor now recombines (family, effort) into one variant via
   resolve_devin_launch_model (cached per pair) before /model, matching the
   launch path. resolve_devin_launch_model degrades to the family if the catalog
   is unreachable, so a probe failure costs effort, not the turn.

Tests: executor composition (compose, mid-chat effort switch re-applies /model,
per-pair cache) and web capability gates (effort + model picker visible for
devin-native in-chat).

Co-authored-by: Isaac <no-reply@databricks.com>
… fixture

Pre-commit (ruff format + end-of-file-newline) flagged two files the earlier
commits added: the subagent reconstruction test's formatting and the captured
node fixture, which json.dump wrote without a trailing newline.

Co-authored-by: Isaac <no-reply@databricks.com>
The composer's slash-command menu unions a session's bundled skills with the
extra skills its harness exposes in its own terminal, resolved per-vendor by
resolve_harness_skills. devin-native had no provider, so it fell through to the
generic host walk and Devin's own skills never appeared.

Devin loads user-invocable skills (slash commands, including plugin-registered
ones) from several of its own dirs (~/.config/devin/skills, ~/.agents/skills,
.devin/skills, …) plus .claude/skills for compatibility, with its own
precedence — more than a single dir walk here would faithfully reproduce. So
devin_host_skills sources the menu authoritatively from `devin skills list
--json --trigger user`, matching exactly the / commands the Devin terminal
offers. Best-effort: a missing CLI / non-zero exit / timeout / bad output
yields no host skills rather than raising, so the bundled skills still show.

The web plumbing (session.skills -> composer menu) is generic, so no web change
is needed; only the runner-side discovery was harness-specific.

Co-authored-by: Isaac <no-reply@databricks.com>
resolve_devin_launch_model is typed str | None (it returns None only for a None
family, which the guard above already excludes), so assigning it into the
str-valued variant cache tripped pyrefly's unsupported-operation. Guard the None
case explicitly — defensively correct and type-clean.

Co-authored-by: Isaac <no-reply@databricks.com>
devin-native declared instruction_delivery=NOT_DELIVERED: the launch path never
even received the agent spec, so a custom Devin agent's AgentSpec.instructions
reached neither the CLI nor the model.

Devin has no --append-system-prompt and ignores config-level instructions
(verified against devin 3000.10.21). Its only channel that reaches every turn's
system prompt is a Windsurf always-on rule (.windsurf/rules/*.md with a
`trigger: always_on` frontmatter — without the frontmatter Devin treats the file
as manual and never loads it; verified the model obeys it with the frontmatter).

So thread ctx.agent_spec into _auto_create_devin_terminal (like pi/cursor) and
write the raw instructions to
<workspace>/.windsurf/rules/omnigent-agent-instructions.md at launch via the new
write_devin_agent_rule. Stable name = one overwritten file, and a plain agent
(no instructions) removes any rule a prior custom-agent launch left, so the
workspace never carries stale instructions. Capability flips to
AGENT_STARTUP_ADDITIVE.

ponytail: the rule writes into the user's workspace — the only always-on channel
Devin exposes (rules are CWD-relative; no out-of-tree rules dir).

Co-authored-by: Isaac <no-reply@databricks.com>
# Conflicts:
#	web/src/shell/NewChatDialog.tsx
Devin's `--permission-mode` was reachable only via `omnigent devin` on the CLI;
the composer had no picker. Add one, stacked on the devin-native PR.

Devin uses the same `--permission-mode <mode>` launch flag as Claude Code, so
the pick rides `terminal_launch_args` exactly like Claude's — but Devin has its
own vocabulary (normal / accept-edits / smart / dangerous) and no running-session
switch. To avoid entangling Claude's mid-chat permission machinery, this is an
isolated `devinPermissionMode` capability mirroring Codex's separate `approvalMode`
pattern rather than reusing `permissionMode`:

- nativeHarnessModes.ts: DEVIN_NATIVE_PERMISSION_MODES (+ default "normal", which
  sends no flag so Devin keeps its own default) with args = ["--permission-mode", v].
- nativeCodingAgents.ts: `devinPermissionMode` capability on the devin row.
- NewChatDialog: the permission hand-menu, chip summary, create-time
  terminal_launch_args emission, draft persistence, and harness-switch reconcile
  all gain a devin branch.

Create-time only: Devin has no `/permissions` popup to drive a live switch.
Server needs no change — terminal_launch_args validation is shape-only and the
runner already appends passthrough to the Devin launch.

Co-authored-by: Isaac <no-reply@databricks.com>
@github-actions github-actions Bot added the size/M Pull request size: M label Sep 13, 2026
@omnigent-ci

omnigent-ci Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Polly AI Review

Review: feat(devin): create-time permission-mode picker in the composer

1. Blocking issues

Permission-mode vocabulary diverges from the harness's own canonical set. DEVIN_NATIVE_PERMISSION_MODES in web/src/lib/nativeHarnessModes.ts defines normal / accept-edits / smart / dangerous, but the stacked base's harness declares the canonical modes as normal / auto / accept-edits / smart / bypass (omnigent/harnesses/devin_native/bridge.py:65), and the base's own docs demonstrate --permission-mode bypass (docs/devin-native.md:90). The consequences are concrete because the flag is forwarded verbatim — the runner reads terminal_launch_args and build_devin_launch_args does args.extend(passthrough) (omnigent/harnesses/devin_native/bridge.py:941) with no normalization or per-harness gating server-side (_validate_terminal_launch_args is shape-and-bounds-only, omnigent/server/routes/_sessions/helpers.py:8649):

  • dangerous is not in the harness's declared set — a user who picks it launches Devin with --permission-mode dangerous, which will likely be rejected by the CLI's argument parsing and fail session startup.
  • The valid, distinct modes auto and bypass are missing from the picker entirely, so real Devin capabilities are unreachable from the composer.

This directly contradicts the PR's own claim that the list was "verified against devin 3000.10.21." The PR should not merge until this discrepancy is resolved: reconcile the frontend list against the actual devin --permission-mode choices for the pinned version and align it with the harness bridge's canonical set (or, if the harness constant is stale, fix both together). The added tests only exercise smart and default-omission (web/src/shell/NewChatDialog.test.tsx:3482), so they cannot catch an invalid or incomplete vocabulary — extend coverage to assert every offered mode maps to a value the harness accepts.

2. Security vulnerabilities

None. terminal_launch_args is validated shape-and-bounds only, and this change adds no new injection surface beyond the fixed, enumerated flag pairs. The mode value is chosen from a closed list, not free-form input.

3. Non-blocking notes

  • The comment at nativeHarnessModes.ts ("Keep in sync with devin --help (auto / accept-edits / smart / dangerous)") is itself internally inconsistent — it names auto while the constant defines normal, and names dangerous which is noncanonical. Fixing Sync #1: Hello world, meet everything since 🌊 #1 should also correct this so the sync note is trustworthy.
  • Minor precision: the description says server validation is "shape-only"; it is actually shape-and-bounds (list length / string-length caps in _validate_terminal_launch_args). Not a defect — the Devin ["--permission-mode", "<mode>"] pair passes cleanly.

4. Approach

The approach is sound and consistent with existing repo patterns. Modeling this as an isolated devinPermissionMode capability that rides terminal_launch_args — mirroring Codex's separate approvalMode and the Cursor/agy exec-mode pickers rather than overloading Claude's permissionMode (which the server hard-gates to claude-native via _PERMISSION_MODE_HARNESS, _session_create_validation.py:199) — is the right call. The UI wiring is complete and symmetric with the sibling capabilities: direct-menu options, selectDirectMode, the Permissions chip, LandingDraft persist/restore, harness reseed, and create-submission emission are all present with no missing supportsDevinPermissionMode branch relative to the other mode pickers.

5. Summary

The plumbing is well-executed and correctly mirrors established capability patterns; server validation, runner passthrough, and UI wiring were all confirmed against source and are substantively correct. The one blocking problem is the mode list itself: DEVIN_NATIVE_PERMISSION_MODES disagrees with the repo's own canonical Devin vocabulary — it ships a dangerous mode the harness does not recognize (likely a launch failure since the flag is forwarded verbatim) and omits the valid auto and bypass modes, and the tests don't cover the offered values. Resolve the vocabulary discrepancy and expand test coverage before merging; otherwise the change is clean.


Automated review by Polly · workflow run

dhruv0811 and others added 3 commits September 14, 2026 16:15
…ve-harness

# Conflicts:
#	web/src/shell/NewChatDialog.tsx
Deprecate the `devin-acp` ACP harness in favor of native `devin-native`, the
backwards-compatible way: keep it registered and resolvable (module, capability,
`--harness devin-acp`, existing sessions), but stop offering it as a fresh pick
so native Devin is the sole "Devin".

- cli_config.py: skip `devin-acp` in the `omnigent config` setup list (the one
  fresh-pick surface that still showed a second "Devin (ACP)" row). Everything
  else in the ACP catalog is untouched.
- acp_cli_harnesses.py / docs/devin-native.md: note the deprecation.
- harness_plugins.py: document the one accepted backwards-compat break — a
  session persisted before this cutover with harness `devin` (which meant ACP
  then) resumes on the native wrap. Not fixable by a static alias (bare `devin`
  must mean native for a fresh launch yet ACP for an old session), and the ACP
  harness shipped only recently, so pre-cutover ACP sessions are thin.
- tests: the setup overview no longer lists "Devin (ACP)"; row-dispatch indices
  shift down by one after native Devin.

Co-authored-by: Isaac <no-reply@databricks.com>
Base automatically changed from dhruvgupta/devin-native-harness to main September 16, 2026 00:39
@dhruv0811 dhruv0811 closed this Oct 3, 2026
@github-actions

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

Closed. If you want to pick this back up, comment /reopen. GitHub only lets maintainers press the Reopen button, so this command does it for you. It needs the source branch to still exist.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/M Pull request size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant