mirror of
https://github.com/stablyai/orca.git
synced 2026-10-27 16:02:42 +00:00
Here is a summary of how the sandbox behaves on your macOS system: ### ⚙️ How it Works When `--sandbox` is enabled (either via the launch flag or the `enableTerminalSandbox` setting in your `settings.json`), terminal commands run inside a lightweight containment boundary: - **macOS Native Isolation**: It utilizes macOS's native `sandbox-exec` utility to restrict system calls, network sockets, and directory access. - **Secure File Boundaries**: File system writes are locked down to designated safe zones (such as your designated workspace or scratch directory). Access to critical system paths, private user data, and external network resources is restricted. --- ### 🛡️ Active Permissions for this Session In this current session, the permission model is configured as follows: | Action / Resource | Permission Status | Details / Paths | | :--- | :--- | :--- | | **Command Execution** | ✅ **Allowed** | Terminal command execution is enabled. | | **File Reads (Allowed)** | ✅ **Allowed** | `/scratch`, `/browser_recordings`, `/html_artifacts`, `/knowledge`, `/worktrees`, `/skills`, `/builtin` | | **File Writes (Allowed)**| ✅ **Allowed** | `/scratch`, `/browser_recordings`, `/html_artifacts`, `/knowledge`, `/worktrees` | | **Sensitive Files** | ⚠️ **Ask** | `.env`, `.npmrc`, `.vscode`, `.git-credentials`, etc. | | **Root/App Settings** | 🚫 **Denied** | Direct modifications to `/config` and main `.gemini` configurations | --- ### 🔧 Configuration and Management * **Persistent Settings**: To enable sandboxing by default for all future sessions, configure the `enableTerminalSandbox` setting in your `~/.gemini/antigravity-cli/settings.json`: ```json { "enableTerminalSandbox": true } ``` * **Dynamic Adjustments**: Within an active CLI (`agy`) session, you can run the `/permissions` slash command to view or modify your autonomy and sandboxing levels on the fly. > [!NOTE] > Running in sandbox mode provides an excellent balance of autonomy and security, allowing me to execute build commands, run test scripts, and manage project files safely without risk to your primary host environment. Please let me know if you would like me to set up a new project workspace or run any specific tasks within this session!
202 lines
6.4 KiB
TypeScript
202 lines
6.4 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest'
|
|
import { PrActionsEngine, type PrActionMutations } from './pr-actions-engine'
|
|
import type { GitHubPrMutationOutcome } from './github-pr-mutations'
|
|
|
|
function deferred<T>() {
|
|
let resolve!: (value: T) => void
|
|
const promise = new Promise<T>((r) => {
|
|
resolve = r
|
|
})
|
|
return { promise, resolve }
|
|
}
|
|
|
|
function makeEngine(mutations: Partial<PrActionMutations>) {
|
|
const ok = async (): Promise<GitHubPrMutationOutcome> => ({ ok: true })
|
|
return new PrActionsEngine({
|
|
mutations: {
|
|
mergePR: ok,
|
|
setPRAutoMerge: ok,
|
|
updatePRState: ok,
|
|
requestReviewers: ok,
|
|
removeReviewers: ok,
|
|
rerunChecks: ok,
|
|
...mutations
|
|
},
|
|
prNumber: 1,
|
|
refetch: () => {},
|
|
onChange: () => {}
|
|
})
|
|
}
|
|
|
|
describe('PrActionsEngine — scoped busy clear (overlapping actions)', () => {
|
|
it('a late-resolving action does not clear a newer action busy state', async () => {
|
|
const slow = deferred<GitHubPrMutationOutcome>()
|
|
const engine = makeEngine({
|
|
// merge resolves slowly; state resolves immediately.
|
|
mergePR: () => slow.promise,
|
|
updatePRState: async () => ({ ok: true })
|
|
})
|
|
|
|
// Start merge (sets busy = merge) but don't let it resolve yet.
|
|
const mergePromise = engine.merge()
|
|
expect(engine.isBusy({ kind: 'merge' })).toBe(true)
|
|
|
|
// While merge is in-flight, run state to completion (sets then clears busy=state).
|
|
await engine.updateState('closed')
|
|
// State began after merge, so it overwrote busy to 'state', then cleared its own.
|
|
expect(engine.isBusy({ kind: 'state' })).toBe(false)
|
|
|
|
// Now let the slow merge finish. Its finally must NOT clear a busy it no longer owns.
|
|
slow.resolve({ ok: true })
|
|
await mergePromise
|
|
// busy stays null (state already cleared); the key point is merge didn't clobber.
|
|
expect(engine.busy).toBeNull()
|
|
})
|
|
|
|
it('clears its own busy when it is still the owner', async () => {
|
|
const engine = makeEngine({})
|
|
await engine.merge()
|
|
expect(engine.busy).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('PrActionsEngine — transport-rejection-normalized outcomes settle cleanly', () => {
|
|
it('routes a { ok:false } outcome to error and clears busy', async () => {
|
|
const onChange = vi.fn()
|
|
const engine = new PrActionsEngine({
|
|
mutations: {
|
|
mergePR: async () => ({ ok: false, error: 'socket hung up' }),
|
|
setPRAutoMerge: async () => ({ ok: true }),
|
|
updatePRState: async () => ({ ok: true }),
|
|
requestReviewers: async () => ({ ok: true }),
|
|
removeReviewers: async () => ({ ok: true }),
|
|
rerunChecks: async () => ({ ok: true })
|
|
},
|
|
prNumber: 1,
|
|
refetch: () => {},
|
|
onChange
|
|
})
|
|
await engine.merge()
|
|
expect(engine.error).toBe('socket hung up')
|
|
expect(engine.busy).toBeNull()
|
|
})
|
|
|
|
it('surfaces a refetch throw as error without rejecting the action promise', async () => {
|
|
const engine = new PrActionsEngine({
|
|
mutations: {
|
|
mergePR: async () => ({ ok: true }),
|
|
setPRAutoMerge: async () => ({ ok: true }),
|
|
updatePRState: async () => ({ ok: true }),
|
|
requestReviewers: async () => ({ ok: true }),
|
|
removeReviewers: async () => ({ ok: true }),
|
|
rerunChecks: async () => ({ ok: true })
|
|
},
|
|
prNumber: 1,
|
|
refetch: async () => {
|
|
throw new Error('refresh failed')
|
|
},
|
|
onChange: () => {}
|
|
})
|
|
await expect(engine.merge()).resolves.toBeUndefined()
|
|
expect(engine.error).toBe('refresh failed')
|
|
expect(engine.busy).toBeNull()
|
|
})
|
|
|
|
it('does not notify on no-op setError(null) at action start', async () => {
|
|
const onChange = vi.fn()
|
|
const engine = new PrActionsEngine({
|
|
mutations: {
|
|
mergePR: async () => ({ ok: true }),
|
|
setPRAutoMerge: async () => ({ ok: true }),
|
|
updatePRState: async () => ({ ok: true }),
|
|
requestReviewers: async () => ({ ok: true }),
|
|
removeReviewers: async () => ({ ok: true }),
|
|
rerunChecks: async () => ({ ok: true })
|
|
},
|
|
prNumber: 1,
|
|
refetch: () => {},
|
|
onChange
|
|
})
|
|
// Idle: error is already null. Action start must only notify for busy, not a
|
|
// redundant clearError path.
|
|
onChange.mockClear()
|
|
const p = engine.merge()
|
|
// First notify is setBusy only (setError(null) no-ops).
|
|
expect(onChange).toHaveBeenCalledTimes(1)
|
|
await p
|
|
})
|
|
})
|
|
|
|
describe('PrActionsEngine — PR identity changes', () => {
|
|
it('clears optimistic state when the engine points at a different PR', async () => {
|
|
const slow = deferred<GitHubPrMutationOutcome>()
|
|
const mutations: PrActionMutations = {
|
|
mergePR: async () => ({ ok: true }),
|
|
setPRAutoMerge: async () => slow.promise,
|
|
updatePRState: async () => ({ ok: true }),
|
|
requestReviewers: async () => ({ ok: true }),
|
|
removeReviewers: async () => ({ ok: true }),
|
|
rerunChecks: async () => ({ ok: true })
|
|
}
|
|
const refetch = vi.fn()
|
|
const onChange = vi.fn()
|
|
const engine = new PrActionsEngine({
|
|
mutations,
|
|
prNumber: 1,
|
|
refetch,
|
|
onChange
|
|
})
|
|
|
|
const action = engine.setAutoMerge(true)
|
|
expect(engine.resolveAutoMerge(false)).toBe(true)
|
|
|
|
engine.updateConfig({
|
|
mutations,
|
|
prNumber: 2,
|
|
refetch,
|
|
onChange
|
|
})
|
|
expect(engine.resolveAutoMerge(false)).toBe(false)
|
|
expect(engine.busy).toBeNull()
|
|
|
|
slow.resolve({ ok: true })
|
|
await action
|
|
expect(refetch).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('clears reviewer optimism when switching PR identity', async () => {
|
|
const slow = deferred<GitHubPrMutationOutcome>()
|
|
const mutations: PrActionMutations = {
|
|
mergePR: async () => ({ ok: true }),
|
|
setPRAutoMerge: async () => ({ ok: true }),
|
|
updatePRState: async () => ({ ok: true }),
|
|
requestReviewers: async () => slow.promise,
|
|
removeReviewers: async () => ({ ok: true }),
|
|
rerunChecks: async () => ({ ok: true })
|
|
}
|
|
const refetch = vi.fn()
|
|
const onChange = vi.fn()
|
|
const engine = new PrActionsEngine({
|
|
mutations,
|
|
prNumber: 1,
|
|
refetch,
|
|
onChange
|
|
})
|
|
|
|
const action = engine.requestReviewer('alice')
|
|
expect(engine.resolveReviewerRequested('alice', false)).toBe(true)
|
|
|
|
engine.updateConfig({
|
|
mutations,
|
|
prNumber: 2,
|
|
refetch,
|
|
onChange
|
|
})
|
|
expect(engine.resolveReviewerRequested('alice', false)).toBe(false)
|
|
|
|
slow.resolve({ ok: true })
|
|
await action
|
|
expect(refetch).not.toHaveBeenCalled()
|
|
})
|
|
})
|