Skip to content

Commit e0e86a9

Browse files
committed
Add verified workspace auto-approval
1 parent 2a811dd commit e0e86a9

14 files changed

Lines changed: 617 additions & 51 deletions

README-2.md

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,8 @@ files and are normalized before Eva uses them.
9090
- LCARS and Eva themes (7 Eva variants)
9191
- Standalone Electron AppImage with bundled bridge
9292
- Durable coding workspaces with automatic Eva-ready Git project provisioning,
93-
isolated worktrees, workspace-confined ACP agents, and durable run records
93+
isolated worktrees, workspace-confined ACP agents, durable run records, and
94+
optional per-run auto approval for remote or otherwise sensitive actions
9495
- Real Electron PTY terminal with xterm rendering, bounded replay, resize/search,
9596
lower-half Workspace Monitor docking, and process-session cancellation
9697
- Main-window Workspace Monitor, unified generated/workspace Assets library,
@@ -1334,16 +1335,30 @@ Workspace agents reuse the observable subagent registry, so the same task is
13341335
visible in Agent Operations with `coding_run_id`, `checkout_id`, and
13351336
`capability_policy`. Unlike generic subagents, the worker creates its
13361337
`ACPClient` with the assigned worktree and uses prompt permission mode
1337-
`workspace_write`.
1338+
`workspace_write`, or `workspace_auto` when **Auto approve actions** is enabled
1339+
for the run. The composer remembers that choice locally for later runs.
13381340

13391341
ACP `session/request_permission` requests are automatically allowed once only
1340-
for `read`, `search`, `fetch`, and `think` tool kinds when the active workspace
1341-
prompt offers an `allow_once` option. Execute, edit, delete, and unknown tool
1342-
kinds require an explicit permission decision because ACP does not provide a
1343-
path contract that can prove worktree confinement. Ordinary chats and generic
1344-
subagents retain interactive permission handling; passive recall continues to
1345-
reject tools. The automatic mode never accepts persistent `allow_always`
1346-
authority.
1342+
for routine workspace reads, local edits, and trusted local build/test commands.
1343+
With `workspace_auto`, Eva also selects one-time approval for remote tools such
1344+
as GitHub CLI issue comments. Auto approval still rejects edits outside the
1345+
assigned worktree and commands referencing protected credential/config paths.
1346+
For an explicit GitHub Issues objective, Eva may use the authenticated `gh`
1347+
CLI. A named issue receives a comment; when no target is named and no matching
1348+
open issue exists, Eva creates a new issue containing the requested report. The
1349+
run completes only after its final `Submitted:` issue/comment URL resolves
1350+
through GitHub. Explicit close and reopen objectives additionally verify the
1351+
issue endpoint reports the requested `closed` or `open` state before completion.
1352+
Failed workspace agents are not automatically replayed on application startup,
1353+
which prevents remote side effects from being duplicated after an ambiguous
1354+
failure. Their retained run exposes an explicit **Retry** action instead.
1355+
Ordinary chats and generic subagents retain interactive permission handling;
1356+
passive recall continues to reject tools. Neither workspace mode accepts
1357+
persistent `allow_always` authority.
1358+
1359+
Settings > Auth launches GitHub CLI device authorization, opens GitHub's device
1360+
page, copies the one-time code when possible, and keeps the code visible with a
1361+
Copy code button until authorization completes.
13471362

13481363
Live ACP chunks update the task and periodically persist a bounded report.
13491364
Plan/tool events update activity. Completion persists the final report and
@@ -1792,7 +1807,7 @@ not a supported agent-state protocol.
17921807
- Bridge binds to `127.0.0.1` by default (localhost only)
17931808
- ACP tool permissions are never globally bypassed. Standalone Eva requires an authenticated in-chat decision; hosted/file clients fail closed.
17941809
- Workspace routes require a second random capability held only by Electron main; the ordinary renderer-visible bridge token is insufficient.
1795-
- Workspace agent `workspace_write` prompts auto-select `allow_once` only for read/search/fetch/think; mutating and unknown tools remain interactive. Normal prompts remain interactive and passive recall remains deny-by-policy.
1810+
- Workspace agent `workspace_write` prompts auto-select `allow_once` for confined reads, edits, and trusted local commands. User-enabled `workspace_auto` also approves other one-time actions, while outside-worktree edits and protected-path commands are rejected by policy. Normal prompts remain interactive and passive recall remains deny-by-policy.
17961811
- Renderer workspace DTOs contain opaque IDs and relative paths only. Known project/worktree paths are redacted from agent reports.
17971812
- Managed worktree paths are revalidated under `EVA_CONFIG_DIR/worktrees` before status, Assets, terminal registration, and cleanup. Runtime-root, intermediate, leaf, and post-registration symlink swaps are rejected.
17981813
- PTY roots are allowlisted; the renderer cannot provide a cwd/environment. PTY shutdown retains Unix session identity and escalates descendants to SIGKILL before root revocation.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
[Website](https://appatalks.github.io/eva-agent/) | [Documentation](README-2.md) | [Issues](https://github.com/appatalks/eva-agent/issues) | License: MIT
66

7-
A voice-first AI assistant for conversation, memory, automation, and local or cloud-backed work. Eva can use voice, camera, browser, desktop, MCP tools, skills, and coding workspaces while keeping control and configuration on your machine.
7+
A voice-first AI assistant for conversation, memory, automation, and local or cloud-backed work. Eva can use voice, camera, browser, desktop, MCP tools, skills, and coding workspaces with optional auto approval while keeping control and configuration on your machine.
88

99
## In Use
1010

core/js/workspaces.js

Lines changed: 82 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,15 @@ var EvaWorkspaces = (function() {
3737
return !!(value && value.workspaceTerminalV1 && value.workspaceListProjects && value.workspaceCreateRun);
3838
}
3939

40+
function autoApprovePreference(value) {
41+
try {
42+
if (typeof value === 'boolean') localStorage.setItem('workspaceAutoApprove', value ? 'true' : 'false');
43+
return localStorage.getItem('workspaceAutoApprove') === 'true';
44+
} catch (_) {
45+
return false;
46+
}
47+
}
48+
4049
function panel() {
4150
return document.getElementById('workspacePanel');
4251
}
@@ -219,6 +228,22 @@ var EvaWorkspaces = (function() {
219228
}
220229
}
221230

231+
async function retryWorkspaceRun(run) {
232+
if (!run || !api() || typeof api().workspaceDispatchRun !== 'function' || state.loading) return;
233+
setBusy(true);
234+
status('Retrying workspace agent...', 'loading');
235+
try {
236+
var updated = await api().workspaceDispatchRun(run.id);
237+
state.selectedProjectId = updated.projectId;
238+
state.selectedRunId = updated.id;
239+
await refresh();
240+
status('Workspace agent retry started.', 'success');
241+
} catch (error) {
242+
status(error.message || 'Workspace agent retry failed.', 'error');
243+
setBusy(false);
244+
}
245+
}
246+
222247
function appendWorkspacePermissions(detail, run) {
223248
var permissions = state.pendingPermissions.filter(function(permission) { return permission.workspaceRunId === run.id; });
224249
if (!permissions.length) return;
@@ -298,6 +323,11 @@ var EvaWorkspaces = (function() {
298323
actions.appendChild(actionButton('Chat', 'Open this run\'s primary chat', function() { loadSession(run.primarySessionId); }));
299324
}
300325
var agentActive = run.agent && ['starting', 'running', 'steering'].indexOf(run.agent.status) !== -1;
326+
if (run.status === 'active' && run.agent && run.agent.status === 'error') {
327+
actions.appendChild(actionButton('Retry', 'Retry this failed workspace run', function() {
328+
retryWorkspaceRun(run);
329+
}));
330+
}
301331
if ((run.status === 'active' || run.status === 'completed') && !agentActive) {
302332
actions.appendChild(actionButton('Archive', 'Keep this run and hide it from active work', function() { applyRunAction(run, 'archive'); }));
303333
actions.appendChild(actionButton('Discard', 'Review removal of this managed worktree', function() {
@@ -638,7 +668,7 @@ var EvaWorkspaces = (function() {
638668
}
639669

640670
function appendWorkbenchRunComposer(detail, project) {
641-
var draft = state.runDrafts[project.id] || (state.runDrafts[project.id] = { objective: '', baseRef: 'HEAD' });
671+
var draft = state.runDrafts[project.id] || (state.runDrafts[project.id] = { objective: '', baseRef: 'HEAD', autoApprove: autoApprovePreference() });
642672
var section = document.createElement('section');
643673
section.className = 'workspace-workbench-section';
644674
var heading = document.createElement('h2');
@@ -660,17 +690,29 @@ var EvaWorkspaces = (function() {
660690
baseRef.maxLength = 256;
661691
baseRef.value = draft.baseRef || 'HEAD';
662692
baseRef.autocomplete = 'off';
693+
var autoApprove = document.createElement('label');
694+
autoApprove.className = 'workspace-auto-approve';
695+
var autoApproveInput = document.createElement('input');
696+
autoApproveInput.type = 'checkbox';
697+
autoApproveInput.checked = draft.autoApprove === true;
698+
var autoApproveText = document.createElement('span');
699+
autoApproveText.textContent = 'Auto approve actions';
700+
autoApprove.append(autoApproveInput, autoApproveText);
663701
objective.addEventListener('input', function() { draft.objective = objective.value; });
664702
baseRef.addEventListener('input', function() { draft.baseRef = baseRef.value; });
703+
autoApproveInput.addEventListener('change', function() {
704+
draft.autoApprove = autoApproveInput.checked;
705+
autoApprovePreference(autoApproveInput.checked);
706+
});
665707
var submit = document.createElement('button');
666708
submit.type = 'submit';
667709
submit.textContent = 'Start isolated run';
668710
submit.disabled = state.loading || !supported();
669-
form.append(objectiveLabel, objective, baseLabel, baseRef, submit);
711+
form.append(objectiveLabel, objective, baseLabel, baseRef, autoApprove, submit);
670712
form.addEventListener('submit', async function(event) {
671713
event.preventDefault();
672714
submit.disabled = true;
673-
var created = await createWorkspaceRun(project.id, objective.value, baseRef.value);
715+
var created = await createWorkspaceRun(project.id, objective.value, baseRef.value, { autoApprove: autoApproveInput.checked });
674716
if (created) {
675717
draft.objective = '';
676718
draft.baseRef = 'HEAD';
@@ -897,6 +939,13 @@ var EvaWorkspaces = (function() {
897939
openWorkspaceTerminal(selected.checkout.id, (selected.project ? selected.project.name + ' | ' : '') + (selected.checkout.branch || 'worktree'));
898940
});
899941
actions.appendChild(terminalButton);
942+
if (selected.status === 'active' && selected.agent && selected.agent.status === 'error') {
943+
var retryButton = document.createElement('button');
944+
retryButton.type = 'button';
945+
retryButton.textContent = 'Retry run';
946+
retryButton.addEventListener('click', function() { retryWorkspaceRun(selected); });
947+
actions.appendChild(retryButton);
948+
}
900949
if (selected.primarySessionId && typeof loadSession === 'function') {
901950
var chatButton = document.createElement('button');
902951
chatButton.type = 'button';
@@ -1257,6 +1306,13 @@ var EvaWorkspaces = (function() {
12571306

12581307
function showGitHubAuthState(authState) {
12591308
var stateValue = authState && authState.state || 'failed';
1309+
var device = document.getElementById('authGitHubDevice');
1310+
var deviceCode = document.getElementById('authGitHubDeviceCode');
1311+
var authStatus = document.getElementById('authGitHubCliStatus');
1312+
var pending = stateValue === 'pending' && authState && authState.code;
1313+
if (device) device.hidden = !pending;
1314+
if (deviceCode) deviceCode.textContent = pending ? authState.code : '';
1315+
if (authStatus) authStatus.textContent = authState && authState.message || '';
12601316
if (stateValue === 'starting') {
12611317
status(authState.message || 'Starting GitHub device authorization...', 'loading');
12621318
return;
@@ -1479,12 +1535,16 @@ var EvaWorkspaces = (function() {
14791535
setBusy(true);
14801536
status('Creating isolated worktree...', 'loading');
14811537
try {
1538+
var autoApprove = options && typeof options.autoApprove === 'boolean'
1539+
? options.autoApprove
1540+
: autoApprovePreference();
14821541
var primarySessionId = typeof _activeSessionId === 'function' ? _activeSessionId() : '';
14831542
var run = await api().workspaceCreateRun({
14841543
projectId: project.id,
14851544
objective: objective,
14861545
primarySessionId: primarySessionId,
1487-
baseRef: String(baseRefValue || '').trim() || 'HEAD'
1546+
baseRef: String(baseRefValue || '').trim() || 'HEAD',
1547+
autoApprove: autoApprove
14881548
});
14891549
state.selectedProjectId = run.projectId;
14901550
state.selectedRunId = run.id;
@@ -1533,10 +1593,12 @@ var EvaWorkspaces = (function() {
15331593
var projectSelect = document.getElementById('workspaceProjectSelect');
15341594
var objective = document.getElementById('workspaceObjective');
15351595
var baseRef = document.getElementById('workspaceBaseRef');
1596+
var autoApprove = document.getElementById('workspaceAutoApprove');
15361597
var created = await createWorkspaceRun(
15371598
projectSelect ? projectSelect.value : '',
15381599
objective ? objective.value : '',
1539-
baseRef ? baseRef.value : 'HEAD'
1600+
baseRef ? baseRef.value : 'HEAD',
1601+
{ autoApprove: !!(autoApprove && autoApprove.checked) }
15401602
);
15411603
if (created && objective) objective.value = '';
15421604
}
@@ -1605,6 +1667,7 @@ var EvaWorkspaces = (function() {
16051667
var workbenchGitHubImport = document.getElementById('workspaceImportGitHubBtn');
16061668
var workbenchGitHubList = document.getElementById('workspaceListGitHubBtn');
16071669
var workbenchGitHubAuth = document.getElementById('authGitHubCliBtn');
1670+
var githubCopyCode = document.getElementById('authGitHubCopyCodeBtn');
16081671
var workbenchGitHubCollapse = document.getElementById('workspaceCollapseGitHubBtn');
16091672
var monitorRefresh = document.getElementById('workspaceMonitorRefreshBtn');
16101673
var monitorClose = document.getElementById('workspaceMonitorCloseBtn');
@@ -1618,6 +1681,15 @@ var EvaWorkspaces = (function() {
16181681
if (workbenchAddProject) workbenchAddProject.addEventListener('click', addProject);
16191682
if (workbenchGitHubImport) workbenchGitHubImport.addEventListener('click', importGitHubProject);
16201683
if (workbenchGitHubAuth) workbenchGitHubAuth.addEventListener('click', authorizeGitHub);
1684+
if (githubCopyCode) githubCopyCode.addEventListener('click', function() {
1685+
var code = document.getElementById('authGitHubDeviceCode');
1686+
if (!code || !code.textContent || !navigator.clipboard) return;
1687+
navigator.clipboard.writeText(code.textContent).then(function() {
1688+
status('GitHub device code copied.', 'success');
1689+
}).catch(function() {
1690+
status('Could not copy the GitHub device code.', 'error');
1691+
});
1692+
});
16211693
if (workbenchGitHubCollapse) workbenchGitHubCollapse.addEventListener('click', collapseGitHubRepositories);
16221694
if (workbenchGitHubList) workbenchGitHubList.addEventListener('click', function() {
16231695
listGitHubRepositories().catch(function(error) { status(error.message || 'GitHub repository listing failed.', 'error'); });
@@ -1634,6 +1706,11 @@ var EvaWorkspaces = (function() {
16341706
});
16351707
if (!supported()) renderUnavailable();
16361708
else {
1709+
var autoApprove = document.getElementById('workspaceAutoApprove');
1710+
if (autoApprove) {
1711+
autoApprove.checked = autoApprovePreference();
1712+
autoApprove.addEventListener('change', function() { autoApprovePreference(autoApprove.checked); });
1713+
}
16371714
monitor();
16381715
state.monitorTimer = setInterval(monitor, 10000);
16391716
}

core/style.css

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,9 @@ body.eva-standalone #settingsMenu {
471471
margin: 0 0 12px;
472472
}
473473
.auth-field { margin-bottom: 12px; }
474+
.auth-device-code { display: flex; align-items: center; gap: 8px; margin: 8px 0; padding: 8px; border: 1px solid #ccc; border-radius: 4px; }
475+
.auth-device-code[hidden] { display: none; }
476+
.auth-device-code strong { flex: 1; font: 700 16px monospace; letter-spacing: 0; }
474477
.auth-input-wrap {
475478
display: flex;
476479
gap: 6px;
@@ -1029,10 +1032,13 @@ body.memory-view-open .memory-inspector-panel .session-panel-header { padding: 1
10291032
.workspace-workbench-section p { margin: 0; color: #84aab0; font-size: 11px; line-height: 1.45; }
10301033
.workspace-workbench-run-form { display: grid; gap: 8px; }
10311034
.workspace-workbench-run-form label { color: #7fa3a9; font: 700 9px monospace; letter-spacing: 0.08em; }
1035+
.workspace-auto-approve { display: flex; align-items: center; gap: 7px; color: inherit; cursor: pointer; }
1036+
.workspace-auto-approve input { width: 15px; height: 15px; margin: 0; accent-color: #78dce8; }
1037+
.workspace-auto-approve span { font-size: 11px; letter-spacing: 0; }
10321038
.workspace-workbench-run-form textarea,
1033-
.workspace-workbench-run-form input { box-sizing: border-box; width: 100%; margin: 0; border: 1px solid rgba(120,220,232,0.22); border-radius: 3px; background: rgba(2,11,14,0.72); color: #e7fbff; font: 12px/1.4 monospace; }
1039+
.workspace-workbench-run-form input[type="text"] { box-sizing: border-box; width: 100%; margin: 0; border: 1px solid rgba(120,220,232,0.22); border-radius: 3px; background: rgba(2,11,14,0.72); color: #e7fbff; font: 12px/1.4 monospace; }
10341040
.workspace-workbench-run-form textarea { min-height: 88px; padding: 8px; resize: vertical; }
1035-
.workspace-workbench-run-form input { height: 32px; padding: 0 8px; }
1041+
.workspace-workbench-run-form input[type="text"] { height: 32px; padding: 0 8px; }
10361042
.workspace-workbench-run-form button { min-height: 34px; border: 1px solid rgba(120,220,232,0.32); border-radius: 3px; background: rgba(120,220,232,0.08); color: #dcfbff; font: 700 10px monospace; cursor: pointer; }
10371043
.workspace-workbench-run-form button:hover:not(:disabled) { border-color: #78dce8; background: rgba(120,220,232,0.15); }
10381044
.workspace-mcp-list { display: grid; gap: 2px; }

index.html

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,10 @@ <h4 class="settings-subhead">My skills</h4>
334334
<select id="workspaceProjectSelect" aria-label="Project for new coding run"></select>
335335
<label for="workspaceObjective">Coding run</label>
336336
<textarea id="workspaceObjective" rows="3" maxlength="4000" placeholder="Describe the change Eva should make"></textarea>
337+
<label class="workspace-auto-approve" for="workspaceAutoApprove">
338+
<input id="workspaceAutoApprove" type="checkbox">
339+
<span>Auto approve actions</span>
340+
</label>
337341
<div class="workspace-run-options">
338342
<label for="workspaceBaseRef">Base</label>
339343
<input id="workspaceBaseRef" type="text" value="HEAD" maxlength="256" autocomplete="off">
@@ -949,7 +953,12 @@ <h2 id="settingsPageTitle">General</h2>
949953
</div>
950954
<p class="auth-note">For private repository imports, grant the selected repository Contents: Read access. Fine-grained tokens must include that repository; classic tokens need the <code>repo</code> scope.</p>
951955
<button id="authGitHubCliBtn" type="button" class="auth-toggle" title="Authorize GitHub CLI using a device code">Authorize GitHub CLI</button>
952-
<p class="auth-note">Eva opens the GitHub device page and copies the one-time code. Complete sign-in in GitHub; the credential stays in GitHub CLI storage.</p>
956+
<div id="authGitHubDevice" class="auth-device-code" hidden aria-live="polite">
957+
<span>Device code</span>
958+
<strong id="authGitHubDeviceCode"></strong>
959+
<button id="authGitHubCopyCodeBtn" type="button" class="auth-toggle">Copy code</button>
960+
</div>
961+
<p id="authGitHubCliStatus" class="auth-note">Eva opens the GitHub device page and shows the one-time code here. Complete sign-in in GitHub; the credential stays in GitHub CLI storage.</p>
953962
</div>
954963

955964
<div class="auth-field">

0 commit comments

Comments
 (0)