Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ tagged release also ships native binaries for Linux, macOS, and Windows.

### Changed

- Added Ask for approval, Approve for me, and Full access presets directly to
the composer’s Research tools menu, with trusted Full access as the default
for new local projects and explicit or managed restrictions preserved.
- Simplified the project sidebar, model and effort controls, chat typography,
sent-message surfaces, and Compute into a quieter results-first workspace.
- Unified logical model names while keeping API-key and ChatGPT access routes
Expand All @@ -27,6 +30,10 @@ tagged release also ships native binaries for Linux, macOS, and Windows.

### Fixed

- Made the research harness normalize WebFetch download destinations, authorize
Explore retrieval consistently, apply multi-file patches transactionally,
resolve the default Python environment, enforce image limits by the active
provider, and accept valid manual-run provenance.
- Hardened research runs against repeated terminal URLs, guessed download-size
escalation, substantially identical timed-out kernel work, stale tool
outcomes, cross-process cancellation races, and orphaned kernel lifecycles.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ See [ARCHITECTURE.md](ARCHITECTURE.md) for how the system fits together, [CONTRI

## Security

The permission system keeps you aware of what the agent is doing; it is not an isolation boundary by itself. OpenScience enables its OS execution sandbox by default: macOS Seatbelt or Linux bubblewrap confines commands to the workspace and approved paths and denies network egress. Routine terminals, kernels, shell commands, and local jobs can run immediately inside that verified boundary; remote jobs, kernel environment changes, project extensions, and host execution still require explicit project trust. The default fails closed when no backend is available, and the sandbox is not a full jail, so run inside a container or VM for hostile code. Managed Atlas tokens stay out of general subprocess environments, arbitrary Python/R kernels receive a minimal environment, and credential-shaped values are redacted from output. To inspect or verify containment, run `openscience sandbox` and `openscience sandbox test`; to report a vulnerability, see [SECURITY.md](SECURITY.md).
The permission system keeps you aware of what the agent is doing; it is not an isolation boundary by itself. New local installs start in **Full access** so a trusted project can use files, commands, and the internet without setup. The composer’s **Research tools → Action approval** menu can switch to **Approve for me** (trusted project inside macOS Seatbelt or Linux bubblewrap) or **Ask for approval** (sandboxed and project trust revoked) at any time. Managed policy may enforce a stricter boundary. The sandbox is not a full jail, so use a container or VM for hostile code. Managed Atlas tokens stay out of general subprocess environments, arbitrary Python/R kernels receive a minimal environment, and credential-shaped values are redacted from output. To inspect or verify containment, run `openscience sandbox` and `openscience sandbox test`; to report a vulnerability, see [SECURITY.md](SECURITY.md).

## License

Expand Down
4 changes: 2 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ OpenScience is an AI agent that runs locally on your machine. The agent can run

### Execution sandbox

The permission system decides whether the agent may take an action. A permission prompt is not an isolation boundary by itself. OpenScience enables its execution sandbox by default and refuses to run when a native backend is unavailable unless you explicitly choose a fallback policy.
The permission system decides whether the agent may take an action. A permission prompt is not an isolation boundary by itself. New local installs default to **Full access**: the project is trusted and the execution sandbox is off. This prioritizes an immediately working research environment and means agent-run commands have your user account's filesystem and network authority.

OpenScience wraps terminal and shell commands, Python/R kernels, and local compute jobs in macOS Seatbelt or Linux bubblewrap. It confines reads and writes to the session workspace and explicitly granted paths and denies network egress. Routine work can run immediately inside that verified boundary; remote jobs, kernel environment changes, project-owned extensions, and host execution still require explicit project trust. Run `openscience sandbox test`; if it does not report **Containment verified**, do not rely on that backend. Windows has no sandbox backend, and the boundary is not a full VM. Use a container or VM for hostile code.
Use **Research tools → Action approval** beneath the composer to switch to **Approve for me** (trust the current project while enabling native containment) or **Ask for approval** (enable containment and revoke project trust). OpenScience then wraps terminal and shell commands, Python/R kernels, and local compute jobs in macOS Seatbelt or Linux bubblewrap, confines reads and writes to the session workspace and explicitly granted paths, and denies network egress. Run `openscience sandbox test`; if it does not report **Containment verified**, do not rely on that backend. Windows has no sandbox backend, and the boundary is not a full VM. Use a container or VM for hostile code.

### Server mode

Expand Down
30 changes: 19 additions & 11 deletions backend/cli/src/agent/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,26 +50,29 @@ export namespace Agent {
export type Info = z.infer<typeof Info>

const compute = async () => {
const cfg = await Config.getExecution()
const [cfg, sandbox] = await Promise.all([Config.getExecution(), Config.trustedSandbox()])
const boundaryAction = sandbox.enabled ? "ask" : "allow"

const defaults = PermissionNext.fromConfig({
"*": "allow",
mcp: "ask",
doom_loop: "ask",
mcp: boundaryAction,
doom_loop: boundaryAction,
external_directory: {
"*": "ask",
"*": boundaryAction,
},
question: "deny",
plan_enter: "deny",
plan_exit: "deny",
compute_job: "ask",
compute_job: boundaryAction,
// mirrors github.com/github/gitignore Node.gitignore pattern for .env files
read: {
"*": "allow",
"*.env": "ask",
"*.env.*": "ask",
"*.env.example": "allow",
},
read: sandbox.enabled
? {
"*": "allow",
"*.env": "ask",
"*.env.*": "ask",
"*.env.example": "allow",
}
: "allow",
})
const user = PermissionNext.fromConfig(cfg.permission ?? {})

Expand Down Expand Up @@ -260,6 +263,10 @@ export namespace Agent {
glob: "allow",
list: "allow",
bash: "allow",
// WebFetch owns a narrowly scoped brokered transfer. Without this
// explicit rule the profile's wildcard deny blocks the broker's
// per-host authorization before the webfetch allow can apply.
network: "allow",
webfetch: "allow",
websearch: "allow",
codesearch: "allow",
Expand All @@ -283,6 +290,7 @@ export namespace Agent {
PermissionNext.fromConfig({
"*": "deny",
bash: "allow",
network: "allow",
read: "allow",
glob: "allow",
grep: "allow",
Expand Down
6 changes: 4 additions & 2 deletions backend/cli/src/agent/prompt/research.txt
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,12 @@ result; internal profiles and skills are implementation details, not user-facing
Kernel state is working memory, not reproducibility: save source, inputs, parameters, and outputs,
and clean-rerun material results when practical.
- Use WebFetch text mode only for bounded pages and APIs. Download large or binary scientific data to a root
basename. For `papers/foo.pdf`, use `output_path:"foo.pdf"`; only after success run sandboxed Bash
`mkdir -p -- 'papers' && test ! -e 'papers/foo.pdf' && mv -- 'foo.pdf' 'papers/foo.pdf'`. Never probe folder paths or send retired
basename; absolute and nested destinations are normalized automatically. For `papers/foo.pdf`, the root download is
`output_path:"foo.pdf"`; only after success run sandboxed Bash
`mkdir -p -- 'papers' && test ! -e 'papers/foo.pdf' && mv -- 'foo.pdf' 'papers/foo.pdf'`. Never send retired
cap/size-evidence fields; WebFetch uses live free disk minus its reserve. Verify the brokered download,
process it locally, and paginate APIs. Do not assume Shell has network access.
- Treat optional binaries as capabilities: probe once, use an installed equivalent when possible, and do not stall on a missing validator. Keep downloaded toolchain caches out of Results.
- Treat an explicitly requested immutable data release as an evidence constraint. If it cannot be
retrieved and verified, disclose that early; stop that branch or clearly bound and label any
live-release fallback rather than silently mixing releases.
Expand Down
4 changes: 2 additions & 2 deletions backend/cli/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -764,7 +764,7 @@ export namespace Config {
.boolean()
.optional()
.describe(
"Run local terminals, kernels, and shell commands inside an OS sandbox (macOS Seatbelt / Linux bubblewrap) that confines writes to authorized project roots. Enabled by default.",
"Run local terminals, kernels, and shell commands inside an OS sandbox (macOS Seatbelt / Linux bubblewrap) that confines writes to authorized project roots. Disabled by default; enable it from the composer or Sandbox settings.",
),
network: z
.enum(["allow", "deny"])
Expand Down Expand Up @@ -1786,7 +1786,7 @@ export namespace Config {
}
const policy = { ...(base ?? {}), ...(managed ?? {}) }
return {
enabled: policy.enabled ?? true,
enabled: policy.enabled ?? false,
network: policy.network ?? "deny",
allowWrite: policy.allowWrite ?? [],
onUnavailable: policy.onUnavailable ?? "error",
Expand Down
18 changes: 14 additions & 4 deletions backend/cli/src/project/trust.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,15 +143,25 @@ export namespace ProjectTrust {
export async function status(project: Project.Info): Promise<Status> {
const canonical = root(project)
const saved = await record(project)
if (!saved || saved.root !== canonical) {
if (!saved) {
return {
projectID: project.id,
root: canonical,
revision: saved?.revision ?? 1,
revision: 1,
state: "trusted",
source: "default",
canExecuteProjectCode: true,
}
}
if (saved.root !== canonical) {
return {
projectID: project.id,
root: canonical,
revision: saved.revision,
state: "untrusted",
source: saved ? "persisted" : "default",
source: "persisted",
canExecuteProjectCode: false,
time: saved?.time,
time: saved.time,
remediation: remediation(project),
}
}
Expand Down
17 changes: 12 additions & 5 deletions backend/cli/src/science/kernel/interpreter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,20 @@ export const KernelEnvironmentName = z
.max(64)
.regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, "Use a simple environment name without path separators")

export function normalizeKernelEnvironmentName(input?: string) {
const value = KernelEnvironmentName.parse(input ?? "python")
return value.toLowerCase() === "default" ? "python" : value
}

export class KernelEnvironmentUnavailable extends Error {
constructor(
readonly environmentName: string,
readonly candidates: string[],
) {
super(
`Python environment '${environmentName}' was not found. Expected an interpreter at ${candidates.join(" or ")}.`,
`Python environment '${environmentName}' was not found. Expected an interpreter at ${candidates.join(" or ")}. ` +
"Omit environment (or use 'default') for the host/conventional .venv runtime. " +
"Named venv or Conda-prefix environments belong at .venv/<name>.",
)
this.name = "KernelEnvironmentUnavailable"
}
Expand All @@ -40,12 +47,12 @@ async function executable(file: string) {
/**
* Resolve a named project Python environment without accepting arbitrary paths.
*
* Named environments live under `.venv/<name>`. The conventional `.venv`
* layout remains a fallback for the default `python` environment so existing
* projects use their dependencies without configuration.
* Named venv or Conda-prefix environments live under `.venv/<name>`. The
* conventional `.venv` layout and host interpreter remain fallbacks for the
* default `python` environment so existing projects work without setup.
*/
export async function pythonEnvironment(projectRoot: string, input?: string): Promise<KernelStartOptions> {
const environmentName = KernelEnvironmentName.parse(input ?? "python")
const environmentName = normalizeKernelEnvironmentName(input)
const roots = [path.join(projectRoot, ".venv", environmentName)]
if (environmentName === "python") roots.push(path.join(projectRoot, ".venv"))
const candidates = roots.map(layout)
Expand Down
4 changes: 2 additions & 2 deletions backend/cli/src/server/routes/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export const ProjectRoutes = lazy(() =>
describeRoute({
summary: "Inspect project trust",
description:
"Inspect whether project-local code may execute. New and relocated projects are untrusted until their canonical root is explicitly approved.",
"Inspect whether project-local code may execute. New projects are trusted by default; an explicit revocation or canonical-root mismatch blocks project code.",
operationId: "project.trust.get",
responses: {
200: {
Expand All @@ -94,7 +94,7 @@ export const ProjectRoutes = lazy(() =>
describeRoute({
summary: "Update project trust",
description:
"Allow project-local code only after submitting the canonical root returned by the trust status endpoint, or revoke that permission immediately.",
"Trust project-local code by submitting the canonical root returned by the status endpoint, or revoke that permission immediately.",
operationId: "project.trust.update",
responses: {
200: {
Expand Down
5 changes: 3 additions & 2 deletions backend/cli/src/session/prompt/core.txt
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,9 @@ verify, and save useful outputs with the smallest sufficient evidence.
expected duration, and estimated price.
- Do not promise isolation, checkpointing, lifecycle, or recovery beyond the active runtime.
- Use WebFetch text mode for bounded text. Download large or binary scientific data to a root basename. For
`papers/foo.pdf`, use `output_path:"foo.pdf"`; only after success run sandboxed Bash
`mkdir -p -- 'papers' && test ! -e 'papers/foo.pdf' && mv -- 'foo.pdf' 'papers/foo.pdf'`. Never probe folder paths or send cap
absolute and nested destinations are reduced to a safe root filename. For `papers/foo.pdf`, the download is
`output_path:"foo.pdf"`; only after success run sandboxed Bash
`mkdir -p -- 'papers' && test ! -e 'papers/foo.pdf' && mv -- 'foo.pdf' 'papers/foo.pdf'`. Never send cap
or size-evidence fields; WebFetch uses live free disk minus its reserve. Paginate APIs; Shell need not have network.
If an immutable release cannot be verified, disclose it and label any live-release fallback.

Expand Down
Loading