-
Notifications
You must be signed in to change notification settings - Fork 2.7k
feat(agent): agentic coding runtime — gated OS capabilities (filesystem, shell, install) via deterministic permission tiers + chat approvals #2631
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
71 commits
Select commit
Hold shift + click to select a range
fe75ea8
feat(security): trusted-roots filesystem access model + enforcement
sanil-23 66c083b
feat(security): hot-swappable live SecurityPolicy + autonomy-changed …
sanil-23 e2b8e43
feat(config): get/update_autonomy_settings RPC for agent access mode
sanil-23 b3a164e
feat(tools): detect_tools + install_tool + Windows PowerShell shell
sanil-23 d5cdd3b
feat(agent): install live policy + inject host-access context at sess…
sanil-23 2cefae9
feat(app): Agent access settings panel + autonomy RPC client
sanil-23 ae4fddd
docs(about_app): catalog agent access mode + document [autonomy] knobs
sanil-23 f7e5c3c
feat(security): Full access mode bypasses the command allowlist
sanil-23 821328b
feat(security): narrow high-risk command set to catastrophic-only
sanil-23 1df3820
feat(security): fail-closed command classifier + harness gate decision
sanil-23 690f89b
feat(tools): route executors through the harness approval gate
sanil-23 a701928
feat(tools): gate write git operations through the approval gate
sanil-23 f75951e
feat(security): route package installs to an always-ask Install bucket
sanil-23 0e128f1
feat(security): cross-platform unconditional path hardening
sanil-23 467a9ae
feat(security): LLM escalate-only command category
sanil-23 efa2185
feat(config): default ~/OpenHuman/projects read-write projects home
sanil-23 88b2715
feat(app): relabel agent access tiers + full-access warning
sanil-23 f5d7308
docs(CLAUDE.md): document the deterministic command permission model
sanil-23 5cdbd87
docs(CLAUDE.md): correct approval-gate status (flag-gated, not absent)
sanil-23 82b9100
Merge remote-tracking branch 'upstream/main' into feat/agentic-runtime
sanil-23 2320b27
feat(approval): thread chat context into the gate for reply routing
sanil-23 d731a65
feat(approval): chat-native yes/no approval surface + ingress router
sanil-23 6fb6d02
style: cargo fmt the agentic-runtime permission changes
sanil-23 3f97992
fix(app): agent-access radio never selected — collapse to 3 tiers
sanil-23 2399821
feat(approval): gate only interactive chat turns, skip background/cron
sanil-23 4c4db08
feat(approval): surface approval_request in chat UI + Approve/Deny
sanil-23 5c388c1
fix(approval): register surface subscriber on serve boot, not only in…
sanil-23 e942fb7
fix(socketio): stop double-emitting streaming deltas via the colon alias
sanil-23 b7faa3f
feat(approval): show the exact command on the card, drop the yes/no n…
sanil-23 140e534
fix(socketio): deliver web-channel events once — socketioxide doesn't…
sanil-23 4b164ba
feat(agent): circuit-break repeated tool failures instead of looping …
sanil-23 2a4fa13
fix(web): rebuild the session agent when the agent-access policy changes
sanil-23 6db3dad
fix(app): auto-apply agent access changes (no separate Save button)
sanil-23 b4f75a1
fix(security): don't block approved redirects (2>&1) — only true hidd…
sanil-23 203ef05
test(web): update SessionCacheFingerprint helper for autonomy_signature
sanil-23 5d18ce7
feat(agent): tell code_executor the command-format constraints + when…
sanil-23 9b35106
fix(runtime): run shell with bash pipefail so masked pipe failures su…
sanil-23 fb863ec
feat(agent): opt-in tool-output logging for debugging (OPENHUMAN_LOG_…
sanil-23 47cdc5d
feat(agent): log tool output in run_inner_loop too (subagent coverage)
sanil-23 7378328
feat(agent): subagent circuit breaker via shared RepeatFailureGuard
sanil-23 e47f453
feat(app): simplify agent-access UI — tier + a single workspace-confi…
sanil-23 ae2dcbb
feat(agent): recognizable hard-reject markers so the agent stops reit…
sanil-23 e3645d4
fix(agent): tag tool-level read-only blocks with [policy-blocked] too
sanil-23 9c4e0b6
test(agent): cover tool-level readonly markers + harness↔gate seam
sanil-23 292880b
chore(agent): remove opt-in tool-output debug logging
sanil-23 08eb4b3
fix(settings): agent-access tier highlight + radio use primary, not d…
sanil-23 ec88ad2
fix(socket): give the core socket.io handshake headroom over high-lat…
sanil-23 3a76dd6
Merge upstream/main into feat/agentic-runtime
sanil-23 f4f6a68
Merge branch 'feat/agentic-runtime' of https://github.com/sanil-23/op…
sanil-23 30ed07a
fix(merge): finish reconciliation + rename Agent access → Agent OS ac…
sanil-23 dd3a3f8
fix(security): always grant ~/OpenHuman/projects, not just on channel…
sanil-23 f52cd2e
style(i18n): prettier-format the mirrored chat.approval locale chunks
sanil-23 0603bea
fix(config): wrap get_autonomy_settings under result (emit a log)
sanil-23 e1ef75a
fix(review): address CodeRabbit — breaker coverage, decide errors, i18n
sanil-23 d109f1d
i18n(settings): localize the whole AgentAccessPanel (CodeRabbit)
sanil-23 7f173dd
fix(shell): audit approved!=allowed — only Prompt-class commands are …
sanil-23 2d8076d
feat(approval): enable the approval gate by default (opt out via =0)
sanil-23 4619a05
fix(agentic-runtime): clear Rust Core Test failures on #2631
sanil-23 54b3f07
Merge remote-tracking branch 'upstream/main' into feat/agentic-runtime
sanil-23 e6908ff
style(agent): rustfmt host_runtime test (wrap long assertion lines)
sanil-23 aae4368
refactor(socketio): explicit STREAMING_DELTA_EVENTS set; doc file_wri…
sanil-23 944e851
fix(approval): gate-flow correctness + stop logging token-derived ses…
sanil-23 cd56a9f
fix(tools): install/detect hardening + autonomy schema alignment
sanil-23 b857f75
fix(ui): approval-card hardening + autonomy save race
sanil-23 3d83364
test(ui): assert localized approval error, not raw RPC text
sanil-23 c3dd2d6
Merge remote-tracking branch 'upstream/main' into feat/agentic-runtime
sanil-23 2b02306
test(ui): use regex matcher + prettier for the localized approval error
sanil-23 51af897
Merge remote-tracking branch 'upstream/main' into feat/agentic-runtime
sanil-23 478c2e3
fix(channels): keep channel-provided tools visible under agent tool-s…
sanil-23 b562271
test(ui): cover AgentAccessPanel + ApprovalRequestCard fallback for d…
sanil-23 1635a32
fix(install_tool): require interactive approval; address review (#2631)
sanil-23 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| import debug from 'debug'; | ||
| import React, { useState } from 'react'; | ||
|
|
||
| import { useT } from '../../lib/i18n/I18nContext'; | ||
| import { callCoreRpc } from '../../services/coreRpcClient'; | ||
| import { clearPendingApprovalForThread, type PendingApproval } from '../../store/chatRuntimeSlice'; | ||
| import { useAppDispatch } from '../../store/hooks'; | ||
| import Button from '../ui/Button'; | ||
|
|
||
| /** | ||
| * Binary v1 decision surface. The backend `approval.decide` RPC also accepts | ||
| * `approve_always_for_tool`, but the locked v1 contract is yes / no — a typed | ||
| * `yes`/`no` chat reply is the equivalent server-side path. | ||
| */ | ||
| const log = debug('openhuman:chat:approval-card'); | ||
|
|
||
| type BinaryDecision = 'approve_once' | 'deny'; | ||
|
|
||
| interface Props { | ||
| threadId: string; | ||
| approval: PendingApproval; | ||
| } | ||
|
|
||
| /** | ||
| * Surfaces a `Prompt`-class tool call parked on the ApprovalGate | ||
| * (`approval_request` socket event) and routes the user's Approve / Deny to the | ||
| * `openhuman.approval_decide` RPC. Rendered above the composer for the active | ||
| * thread; clears itself on a recorded decision (the turn-end handlers in | ||
| * {@link ChatRuntimeProvider} also clear it if the turn is cancelled). | ||
| */ | ||
| export const ApprovalRequestCard: React.FC<Props> = ({ threadId, approval }) => { | ||
| const { t } = useT(); | ||
| const dispatch = useAppDispatch(); | ||
| const [deciding, setDeciding] = useState<BinaryDecision | null>(null); | ||
| const [errorMsg, setErrorMsg] = useState<string | null>(null); | ||
|
|
||
| const decide = async (decision: BinaryDecision) => { | ||
| if (deciding) return; | ||
| setDeciding(decision); | ||
| setErrorMsg(null); | ||
| try { | ||
| await callCoreRpc({ | ||
| method: 'openhuman.approval_decide', | ||
| params: { request_id: approval.requestId, decision }, | ||
| }); | ||
| // Resolve optimistically; ChatRuntimeProvider also clears on turn end. | ||
| dispatch(clearPendingApprovalForThread({ threadId })); | ||
| } catch (e) { | ||
| // Keep raw RPC error detail in namespaced dev logs only; show the user the | ||
| // localized fallback — never leak internal error text into the UI. | ||
| log('approval_decide failed: %o', e); | ||
| setErrorMsg(t('chat.approval.error')); | ||
| setDeciding(null); | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <div | ||
| role="alertdialog" | ||
| aria-label={t('chat.approval.title')} | ||
| className="rounded-xl border border-amber/40 bg-amber/5 p-3 text-sm"> | ||
| <div className="flex items-start gap-2"> | ||
| <span aria-hidden className="text-base leading-none"> | ||
| 🔒 | ||
| </span> | ||
| <div className="min-w-0 flex-1"> | ||
| <p className="font-semibold text-ink">{t('chat.approval.title')}</p> | ||
| <p className="mt-1 text-ink-soft break-words"> | ||
| {approval.message || t('chat.approval.fallback')} | ||
| </p> | ||
| {approval.command && ( | ||
| <pre className="mt-2 max-h-40 overflow-auto whitespace-pre-wrap break-all rounded bg-ink/5 px-2 py-1.5 font-mono text-xs text-ink"> | ||
| {approval.command} | ||
| </pre> | ||
| )} | ||
| <p className="mt-1 text-xs text-ink-soft"> | ||
| {t('chat.approval.tool')}{' '} | ||
| <span className="font-mono text-ink">{approval.toolName}</span> | ||
| </p> | ||
|
|
||
| {errorMsg && <p className="mt-2 text-xs text-coral">⚠ {errorMsg}</p>} | ||
|
|
||
| <div className="mt-3 flex items-center gap-2"> | ||
| <Button | ||
| variant="primary" | ||
| size="sm" | ||
| onClick={() => void decide('approve_once')} | ||
| disabled={deciding !== null}> | ||
| {deciding === 'approve_once' | ||
| ? t('chat.approval.deciding') | ||
| : t('chat.approval.approve')} | ||
| </Button> | ||
| <Button | ||
| variant="secondary" | ||
| size="sm" | ||
| onClick={() => void decide('deny')} | ||
| disabled={deciding !== null}> | ||
| {deciding === 'deny' ? t('chat.approval.deciding') : t('chat.approval.deny')} | ||
| </Button> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default ApprovalRequestCard; | ||
112 changes: 112 additions & 0 deletions
112
app/src/components/chat/__tests__/ApprovalRequestCard.test.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| import { configureStore } from '@reduxjs/toolkit'; | ||
| import { fireEvent, render, screen, waitFor } from '@testing-library/react'; | ||
| import { Provider } from 'react-redux'; | ||
| import { beforeEach, describe, expect, it, vi } from 'vitest'; | ||
|
|
||
| import { callCoreRpc } from '../../../services/coreRpcClient'; | ||
| import chatRuntimeReducer, { | ||
| type PendingApproval, | ||
| setPendingApprovalForThread, | ||
| } from '../../../store/chatRuntimeSlice'; | ||
| import ApprovalRequestCard from '../ApprovalRequestCard'; | ||
|
|
||
| vi.mock('../../../services/coreRpcClient', () => ({ callCoreRpc: vi.fn() })); | ||
|
|
||
| const THREAD = 't1'; | ||
| const approval: PendingApproval = { | ||
| requestId: 'req-approval-1', | ||
| toolName: 'shell', | ||
| message: 'Run `shell` — shell (18 bytes of arguments)', | ||
| command: 'pip show yfinance', | ||
| }; | ||
|
|
||
| function renderCard() { | ||
| const store = configureStore({ reducer: { chatRuntime: chatRuntimeReducer } }); | ||
| store.dispatch(setPendingApprovalForThread({ threadId: THREAD, approval })); | ||
| const utils = render( | ||
| <Provider store={store}> | ||
| <ApprovalRequestCard threadId={THREAD} approval={approval} /> | ||
| </Provider> | ||
| ); | ||
| return { store, ...utils }; | ||
| } | ||
|
sanil-23 marked this conversation as resolved.
|
||
|
|
||
| describe('ApprovalRequestCard', () => { | ||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| }); | ||
|
|
||
| it('renders the action summary, exact command, and tool name', () => { | ||
| renderCard(); | ||
| expect(screen.getByText('Approval needed')).toBeInTheDocument(); | ||
| expect(screen.getByText('Run `shell` — shell (18 bytes of arguments)')).toBeInTheDocument(); | ||
| // The exact command being requested is shown verbatim. | ||
| expect(screen.getByText('pip show yfinance')).toBeInTheDocument(); | ||
| expect(screen.getByText('shell')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('does not nudge the user to reply yes/no (buttons are the input path)', () => { | ||
| renderCard(); | ||
| expect(screen.queryByText(/reply.*yes/i)).not.toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('Approve routes approve_once to approval_decide and clears the pending state', async () => { | ||
| vi.mocked(callCoreRpc).mockResolvedValueOnce({}); | ||
| const { store } = renderCard(); | ||
|
|
||
| fireEvent.click(screen.getByText('Approve')); | ||
|
|
||
| expect(callCoreRpc).toHaveBeenCalledWith({ | ||
| method: 'openhuman.approval_decide', | ||
| params: { request_id: 'req-approval-1', decision: 'approve_once' }, | ||
| }); | ||
| await waitFor(() => { | ||
| expect(store.getState().chatRuntime.pendingApprovalByThread[THREAD]).toBeUndefined(); | ||
| }); | ||
| }); | ||
|
|
||
| it('Deny routes deny to approval_decide', async () => { | ||
| vi.mocked(callCoreRpc).mockResolvedValueOnce({}); | ||
| const { store } = renderCard(); | ||
|
|
||
| fireEvent.click(screen.getByText('Deny')); | ||
|
|
||
| expect(callCoreRpc).toHaveBeenCalledWith({ | ||
| method: 'openhuman.approval_decide', | ||
| params: { request_id: 'req-approval-1', decision: 'deny' }, | ||
| }); | ||
| await waitFor(() => { | ||
| expect(store.getState().chatRuntime.pendingApprovalByThread[THREAD]).toBeUndefined(); | ||
| }); | ||
| }); | ||
|
|
||
| it('keeps the prompt and shows an error when the decide RPC fails', async () => { | ||
| vi.mocked(callCoreRpc).mockRejectedValueOnce(new Error('gate not installed')); | ||
| const { store } = renderCard(); | ||
|
|
||
| fireEvent.click(screen.getByText('Approve')); | ||
|
|
||
| await waitFor(() => { | ||
| // Raw RPC error text ('gate not installed') is no longer surfaced to the | ||
| // user — it's kept in a namespaced debug log; the localized fallback shows. | ||
| expect(screen.getByText(/Could not record your decision/)).toBeInTheDocument(); | ||
| }); | ||
| // Decision failed → approval stays parked, buttons remain actionable. | ||
| expect(store.getState().chatRuntime.pendingApprovalByThread[THREAD]).toEqual(approval); | ||
| expect(screen.getByText('Approve')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('falls back to the generic prompt when the approval has no message', () => { | ||
| const store = configureStore({ reducer: { chatRuntime: chatRuntimeReducer } }); | ||
| const noMessage: PendingApproval = { ...approval, message: '' }; | ||
| store.dispatch(setPendingApprovalForThread({ threadId: THREAD, approval: noMessage })); | ||
| render( | ||
| <Provider store={store}> | ||
| <ApprovalRequestCard threadId={THREAD} approval={noMessage} /> | ||
| </Provider> | ||
| ); | ||
| expect( | ||
| screen.getByText('The agent wants to run an action that needs your approval.') | ||
| ).toBeInTheDocument(); | ||
| }); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.