mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-08-16 09:51:59 +00:00
Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
403dec8e98 | ||
|
|
2c7cf6118c | ||
|
|
2bdd860b54 | ||
|
|
81f1ffbb4f | ||
|
|
93fc7b9e77 | ||
|
|
9fc5b875d1 | ||
|
|
08279e6b99 | ||
|
|
a7c31e89b4 | ||
|
|
04014c658a | ||
|
|
c1238d3e7e | ||
|
|
687e80a55a | ||
|
|
b90fd01af2 | ||
|
|
bbe7df7d33 |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"label": "Git Clones",
|
||||
"message": "166,579",
|
||||
"message": "175,911",
|
||||
"color": "green",
|
||||
"namedLogo": "git"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"total_clones": 166579,
|
||||
"last_updated": "2026-07-20T08:53:12Z",
|
||||
"total_clones": 175911,
|
||||
"last_updated": "2026-07-28T08:29:18Z",
|
||||
"daily": {
|
||||
"2026-03-27": 2189,
|
||||
"2026-03-28": 1874,
|
||||
@@ -116,6 +116,14 @@
|
||||
"2026-07-16": 2497,
|
||||
"2026-07-17": 1773,
|
||||
"2026-07-18": 1542,
|
||||
"2026-07-19": 1445
|
||||
"2026-07-19": 1445,
|
||||
"2026-07-20": 1481,
|
||||
"2026-07-21": 1528,
|
||||
"2026-07-22": 1529,
|
||||
"2026-07-23": 1209,
|
||||
"2026-07-24": 1118,
|
||||
"2026-07-25": 928,
|
||||
"2026-07-26": 740,
|
||||
"2026-07-27": 799
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# Full system access: unrestricted shell and filesystem
|
||||
# Copy to ~/.openjarvis/config.toml
|
||||
#
|
||||
# WARNING: shell_exec runs arbitrary commands as your user. No command
|
||||
# allowlist, no denylist, no working-directory restriction. file_read and
|
||||
# file_write aren't restricted to any directory either. Only enable what you
|
||||
# actually want the agent to have. tools.enabled is the whole permission grant;
|
||||
# there's no second allowlist to configure.
|
||||
#
|
||||
# On macOS, this config alone does not reach TCC-protected data (Messages,
|
||||
# Mail, Photos, Safari). That requires Full Disk Access granted to the process
|
||||
# hosting the backend. See docs/user-guide/system-access.md.
|
||||
#
|
||||
# Usage:
|
||||
# jarvis ask "What's using the most disk space in my home directory?"
|
||||
# jarvis chat # prompts before each shell_exec call
|
||||
|
||||
[engine]
|
||||
default = "ollama"
|
||||
|
||||
[intelligence]
|
||||
default_model = "qwen3.5:9b"
|
||||
|
||||
[agent]
|
||||
default_agent = "orchestrator"
|
||||
max_turns = 10
|
||||
|
||||
[tools]
|
||||
enabled = [
|
||||
"shell_exec",
|
||||
"file_read",
|
||||
"file_write",
|
||||
"apply_patch",
|
||||
"code_interpreter",
|
||||
"git_status",
|
||||
"git_diff",
|
||||
"think",
|
||||
"calculator",
|
||||
]
|
||||
@@ -604,7 +604,7 @@ enforce_tool_confirmation = true
|
||||
| `scan_output` | bool | `true` | Whether to scan model output. |
|
||||
| `secret_scanner` | bool | `true` | Enable secret detection (API keys, tokens, passwords). |
|
||||
| `pii_scanner` | bool | `true` | Enable PII detection (emails, SSNs, credit cards). |
|
||||
| `enforce_tool_confirmation` | bool | `true` | Require confirmation before executing tools. |
|
||||
| `enforce_tool_confirmation` | bool | `true` | Accepted but **not currently enforced**. Whether you get prompts depends on the entry point. See [System Access](../user-guide/system-access.md#confirmation-behaviour). |
|
||||
|
||||
!!! tip "Choosing a security mode"
|
||||
Use `"warn"` during development to see what would be flagged without disrupting output.
|
||||
|
||||
@@ -392,7 +392,7 @@ enforce_tool_confirmation = true
|
||||
| `secret_scanner` | `bool` | `true` | Run `SecretScanner` on all text |
|
||||
| `pii_scanner` | `bool` | `true` | Run `PIIScanner` on all text |
|
||||
| `audit_log_path` | `str` | `~/.openjarvis/audit.db` | Path to the SQLite audit log |
|
||||
| `enforce_tool_confirmation` | `bool` | `true` | Require explicit confirmation before tool execution |
|
||||
| `enforce_tool_confirmation` | `bool` | `true` | Accepted by the loader but **not currently enforced**. See [System Access](system-access.md#confirmation-behaviour) for when prompts actually happen |
|
||||
|
||||
!!! tip "Start with warn, tighten later"
|
||||
`mode = "warn"` is a good starting point. It lets you observe what patterns are being triggered without disrupting normal usage. Switch to `"redact"` once you are satisfied that the scanner isn't producing too many false positives for your workload.
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
# System Access
|
||||
|
||||
How to give an agent access to the machine it runs on, and where the real
|
||||
limits are.
|
||||
|
||||
!!! warning
|
||||
`shell_exec` runs arbitrary commands as your user. There is no command
|
||||
allowlist, no denylist, and no sandbox unless you turn one on. An agent
|
||||
holding this tool can do anything you can do from a terminal.
|
||||
|
||||
---
|
||||
|
||||
## Start here: you probably have no tools enabled
|
||||
|
||||
If the agent tells you it can't run commands or read files, that's usually not
|
||||
a permissions problem. It means no tools were enabled in the first place.
|
||||
|
||||
Tools come from `tools.enabled`, falling back to `agent.tools`. Both default to
|
||||
empty, and an empty value builds the agent with **zero tools**. Nothing is
|
||||
enabled by default.
|
||||
|
||||
First check whether you have a config file at all:
|
||||
|
||||
```bash
|
||||
cat ~/.openjarvis/config.toml
|
||||
```
|
||||
|
||||
If it isn't there, that's your answer. Create it:
|
||||
|
||||
```toml
|
||||
[engine]
|
||||
default = "ollama"
|
||||
|
||||
[intelligence]
|
||||
default_model = "qwen3.5:9b"
|
||||
|
||||
[agent]
|
||||
default_agent = "orchestrator"
|
||||
|
||||
[tools]
|
||||
enabled = ["shell_exec", "file_read", "file_write", "think"]
|
||||
```
|
||||
|
||||
There's a fuller version at
|
||||
`configs/openjarvis/examples/full-system-access.toml`.
|
||||
|
||||
Then confirm the list actually resolved:
|
||||
|
||||
```bash
|
||||
python -c "from openjarvis.core.config import load_config; print(load_config().tools.enabled)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What the tools reach
|
||||
|
||||
| Tool | Scope |
|
||||
|------|-------|
|
||||
| `shell_exec` | Any command, as your user. 30s default timeout, 300s max, output capped at 100 KB per stream. |
|
||||
| `file_read` | Any readable path. 1 MB cap. |
|
||||
| `file_write` | Any writable path. 10 MB cap, can create parent directories. |
|
||||
| `apply_patch` | Applies unified diffs to any path. |
|
||||
| `code_interpreter` | Python in a subprocess, behind a coarse pattern blocklist. |
|
||||
|
||||
`file_read` and `file_write` take an `allowed_dirs` argument that limits them to
|
||||
a set of directories, but no config key populates it. When it's empty every path
|
||||
is allowed. If you want a filesystem jail today, use the container sandbox
|
||||
instead of relying on these tools to enforce one.
|
||||
|
||||
### Sensitive filenames
|
||||
|
||||
`file_read` and `file_write` refuse names matching a short glob list: `.env`,
|
||||
`*.pem`, `id_rsa`, `credentials.*` and a dozen or so others. It matches on the
|
||||
filename only, not the path or the contents, and only those two tools consult
|
||||
it. `shell_exec`, `apply_patch` and `code_interpreter` skip it entirely, so
|
||||
`cat ~/.ssh/id_rsa` through `shell_exec` works fine. Treat it as protection
|
||||
against fat fingers, not as a security boundary.
|
||||
|
||||
---
|
||||
|
||||
## Confirmation behaviour
|
||||
|
||||
`shell_exec`, `git_commit` and `agent_kill` are marked `requires_confirmation`.
|
||||
What that translates to depends entirely on how you launched the agent:
|
||||
|
||||
| Entry point | Behaviour |
|
||||
|-------------|-----------|
|
||||
| `jarvis chat` | Prompts before each call. |
|
||||
| `jarvis ask` | Auto-approves. |
|
||||
| `jarvis agent ask` | Auto-approves. Pass `--no-yes` if you want prompts. |
|
||||
| HTTP server, desktop app | Auto-approves. Tools you added to an agent's toolkit count as pre-approved. |
|
||||
| Embedded via `SystemBuilder` | No callback is wired, so these tools fail closed. |
|
||||
|
||||
That last row catches people out. If `shell_exec` returns "requires
|
||||
confirmation but no confirmation callback is available", you're constructing the
|
||||
agent yourself and need to pass a `confirm_callback`.
|
||||
|
||||
!!! note "`enforce_tool_confirmation` doesn't do anything"
|
||||
The config loader accepts `security.enforce_tool_confirmation`, but nothing
|
||||
on the tool execution path reads it. Setting it won't change confirmation
|
||||
behaviour anywhere. Use the table above instead.
|
||||
|
||||
---
|
||||
|
||||
## macOS: Full Disk Access
|
||||
|
||||
On macOS the operating system is the real boundary, not the config. Shell
|
||||
access and ordinary file access start working as soon as you enable the tools.
|
||||
TCC-protected data does not: Messages, Mail, Photos, Safari history, Contacts
|
||||
and Calendar all stay locked, and no config key will change that.
|
||||
|
||||
Grant Full Disk Access to whichever process hosts the backend. Child processes
|
||||
inherit it:
|
||||
|
||||
| How you run OpenJarvis | Grant access to |
|
||||
|------------------------|-----------------|
|
||||
| CLI (`jarvis ask`, `jarvis chat`) | Your terminal (Terminal, iTerm, Warp) |
|
||||
| Desktop app | `OpenJarvis.app`, which spawns `jarvis serve` beneath it |
|
||||
| launchd (`deploy/launchd/com.openjarvis.plist`) | The `jarvis` binary, as its own entry |
|
||||
|
||||
System Settings, then Privacy & Security, then Full Disk Access, then **+**.
|
||||
|
||||
A launchd daemon gets its own TCC context, so granting access to Terminal does
|
||||
nothing for it. Add `/usr/local/bin/jarvis` separately.
|
||||
|
||||
To check whether the grant took:
|
||||
|
||||
```bash
|
||||
head -c 16 ~/Library/Messages/chat.db >/dev/null 2>&1 \
|
||||
&& echo "granted" || echo "denied"
|
||||
```
|
||||
|
||||
Restart the host process after you change the setting.
|
||||
|
||||
### Driving Mac apps
|
||||
|
||||
AppleScript works through `shell_exec`:
|
||||
|
||||
```
|
||||
osascript -e 'tell application "Music" to play'
|
||||
```
|
||||
|
||||
macOS asks for Automation permission once per target app, the first time you
|
||||
touch it.
|
||||
|
||||
---
|
||||
|
||||
## What you can't do
|
||||
|
||||
There's no computer use. OpenJarvis can't see your screen, move the pointer or
|
||||
send keystrokes. No tool for it is registered and no input automation library
|
||||
appears anywhere in the codebase, so granting Accessibility or Screen Recording
|
||||
buys you nothing on its own.
|
||||
|
||||
The `click` and `type` actions you'll find are Playwright, scoped to a browser
|
||||
page rather than the desktop.
|
||||
|
||||
Some of this is reachable through `shell_exec` if you bring the tooling
|
||||
yourself. `screencapture` will take screenshots once you've granted Screen
|
||||
Recording, and something like `cliclick` will move the pointer. That gets you
|
||||
scripted actions. It doesn't get you an agent that looks at the screen and
|
||||
works out where to click.
|
||||
|
||||
---
|
||||
|
||||
## Narrowing access
|
||||
|
||||
Access widens and narrows through `tools.enabled`. Drop entries to take
|
||||
capabilities away. That list is the whole grant.
|
||||
|
||||
Two stronger isolation options exist. Both are off by default:
|
||||
|
||||
```toml
|
||||
[sandbox]
|
||||
enabled = true # run tools inside a container
|
||||
runtime = "docker"
|
||||
|
||||
[security.capabilities]
|
||||
enabled = true # RBAC over declared tool capabilities
|
||||
policy_path = "~/.openjarvis/policy.yaml"
|
||||
```
|
||||
|
||||
!!! note "Capabilities are open by default even once enabled"
|
||||
`CapabilityPolicy` is built with `default_deny=False` and no config key
|
||||
exposes that flag, so an agent with no explicit policy entry gets every
|
||||
capability. Write entries for every agent you mean to restrict.
|
||||
|
||||
For anything untrusted, reach for `docker_shell_exec` and
|
||||
`code_interpreter_docker` rather than the host-side versions.
|
||||
|
||||
---
|
||||
|
||||
## See also
|
||||
|
||||
- [Security](security.md) for scanners, the audit log and guardrails
|
||||
- [Tools](tools.md) for the full registry
|
||||
- [Code Assistant](code-assistant.md) for a narrower shell-enabled setup
|
||||
- [External MCP Servers](mcp-external-servers.md) for capabilities OpenJarvis doesn't ship
|
||||
@@ -196,6 +196,7 @@ nav:
|
||||
- Telemetry: user-guide/telemetry.md
|
||||
- Evaluations: user-guide/evaluations.md
|
||||
- Benchmarks: user-guide/benchmarks.md
|
||||
- System Access: user-guide/system-access.md
|
||||
- Security: user-guide/security.md
|
||||
- LLM-guided spec search: user-guide/llm-guided-spec-search.md
|
||||
- Leaderboard: leaderboard.md
|
||||
|
||||
@@ -81,14 +81,28 @@ def start(
|
||||
if agent_name:
|
||||
cmd.extend(["--agent", agent_name])
|
||||
|
||||
# Start as background process
|
||||
# Start as background process, fully detached from the launching terminal.
|
||||
#
|
||||
# ``start_new_session`` is POSIX-only: CPython's Windows ``_execute_child``
|
||||
# names the parameter ``unused_start_new_session`` and ignores it. Relying
|
||||
# on it there leaves the server sharing its parent's console, so closing
|
||||
# that console — or logging off — delivers CTRL_CLOSE_EVENT and kills the
|
||||
# daemon. DETACHED_PROCESS gives it no console at all; the new process
|
||||
# group additionally stops a Ctrl-C in the parent reaching it.
|
||||
DEFAULT_CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
log_fh = open(_LOG_FILE, "a") # noqa: SIM115
|
||||
spawn_kwargs: dict = {}
|
||||
if sys.platform == "win32":
|
||||
spawn_kwargs["creationflags"] = (
|
||||
subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP
|
||||
)
|
||||
else:
|
||||
spawn_kwargs["start_new_session"] = True
|
||||
proc = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=log_fh,
|
||||
stderr=log_fh,
|
||||
start_new_session=True,
|
||||
**spawn_kwargs,
|
||||
)
|
||||
_write_pid(proc.pid)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
@@ -79,3 +80,80 @@ class TestDaemonCommands:
|
||||
result = CliRunner().invoke(cli, ["start"])
|
||||
assert result.exit_code != 0
|
||||
assert "already running" in result.output
|
||||
|
||||
|
||||
class TestDaemonDetachment:
|
||||
"""The spawned server must outlive the console that started it.
|
||||
|
||||
``start_new_session`` is POSIX-only — CPython's Windows ``_execute_child``
|
||||
names the parameter ``unused_start_new_session``. Relying on it there leaves
|
||||
the server sharing its parent's console, so closing that console (or logging
|
||||
off) delivers CTRL_CLOSE_EVENT and kills the daemon.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _spawn_kwargs(platform: str) -> dict:
|
||||
"""Return the kwargs ``start`` passes to Popen when spawning the server.
|
||||
|
||||
``load_config`` is stubbed because it shells out for GPU detection —
|
||||
patching Popen wholesale would otherwise break config loading before
|
||||
the spawn is reached.
|
||||
"""
|
||||
with (
|
||||
patch("openjarvis.cli.daemon_cmd._read_pid", return_value=None),
|
||||
patch("openjarvis.cli.daemon_cmd._write_pid"),
|
||||
patch("openjarvis.cli.daemon_cmd.load_config"),
|
||||
patch("openjarvis.cli.daemon_cmd.sys.platform", platform),
|
||||
patch("openjarvis.cli.daemon_cmd.subprocess.Popen") as popen,
|
||||
patch("builtins.open", MagicMock()),
|
||||
):
|
||||
popen.return_value = MagicMock(pid=4321)
|
||||
result = CliRunner().invoke(cli, ["start"])
|
||||
assert result.exit_code == 0, result.output
|
||||
spawns = [
|
||||
c for c in popen.call_args_list if c.args and "serve" in c.args[0]
|
||||
]
|
||||
assert spawns, f"start did not spawn the server: {popen.call_args_list}"
|
||||
return spawns[-1].kwargs
|
||||
|
||||
def test_windows_spawn_is_detached_from_the_console(self) -> None:
|
||||
# These constants are only exported by ``subprocess`` on Windows.
|
||||
# Supply their documented values so the simulated Windows branch is
|
||||
# still exercised by the POSIX test job.
|
||||
detached_process = getattr(subprocess, "DETACHED_PROCESS", 0x00000008)
|
||||
create_new_process_group = getattr(
|
||||
subprocess, "CREATE_NEW_PROCESS_GROUP", 0x00000200
|
||||
)
|
||||
with (
|
||||
patch.object(
|
||||
subprocess,
|
||||
"DETACHED_PROCESS",
|
||||
detached_process,
|
||||
create=True,
|
||||
),
|
||||
patch.object(
|
||||
subprocess,
|
||||
"CREATE_NEW_PROCESS_GROUP",
|
||||
create_new_process_group,
|
||||
create=True,
|
||||
),
|
||||
):
|
||||
kwargs = self._spawn_kwargs("win32")
|
||||
|
||||
flags = kwargs.get("creationflags", 0)
|
||||
assert flags & detached_process, (
|
||||
"server must be spawned with DETACHED_PROCESS on Windows, otherwise "
|
||||
"closing the launching console kills it"
|
||||
)
|
||||
assert flags & create_new_process_group, (
|
||||
"server must be in its own process group so Ctrl-C in the parent "
|
||||
"console does not propagate to it"
|
||||
)
|
||||
assert not kwargs.get("start_new_session"), (
|
||||
"start_new_session is ignored on Windows; it must not be relied on"
|
||||
)
|
||||
|
||||
def test_posix_spawn_still_uses_start_new_session(self) -> None:
|
||||
kwargs = self._spawn_kwargs("linux")
|
||||
assert kwargs.get("start_new_session") is True
|
||||
assert "creationflags" not in kwargs or kwargs["creationflags"] == 0
|
||||
|
||||
@@ -37,16 +37,22 @@ class TestAgentRoutes:
|
||||
|
||||
|
||||
class TestMemoryRoutes:
|
||||
# 503 is the documented response when the native ``openjarvis_rust``
|
||||
# extension is absent from the venv (see TestMemoryRustMissing below).
|
||||
# These tests are only asserting "the route is wired up", so a backend
|
||||
# that cannot be built is tolerated the same way a 500 is.
|
||||
_BACKEND_OPTIONAL = (200, 500, 503)
|
||||
|
||||
def test_search(self):
|
||||
client = TestClient(_make_app())
|
||||
resp = client.post("/v1/memory/search", json={"query": "test"})
|
||||
# May fail if SQLite not set up, that's ok
|
||||
assert resp.status_code in (200, 500)
|
||||
assert resp.status_code in self._BACKEND_OPTIONAL
|
||||
|
||||
def test_stats(self):
|
||||
client = TestClient(_make_app())
|
||||
resp = client.get("/v1/memory/stats")
|
||||
assert resp.status_code in (200, 500)
|
||||
assert resp.status_code in self._BACKEND_OPTIONAL
|
||||
|
||||
|
||||
class TestMemoryRustMissing:
|
||||
|
||||
Reference in New Issue
Block a user