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
17 changes: 14 additions & 3 deletions .opencode-plugin/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,15 +89,26 @@ test("wrapContext: tier-specific hint inside the tags, content untouched", () =>
assert.equal(wrapContext(body, false), `<orbit-context>\n${CRUISE_HINT}\n${body}\n</orbit-context>`)
})

test("allowsOrbitCommand: safe tiers auto-approved", () => {
test("allowsOrbitCommand: framework-verified tiers auto-approved", () => {
assert.equal(allowsOrbitCommand("orbit status"), true)
assert.equal(allowsOrbitCommand("orbit memo backend"), true)
assert.equal(allowsOrbitCommand("orbit.sh jot backend \"discovery\""), true)
assert.equal(allowsOrbitCommand("/usr/local/bin/orbit context --startup"), true)
})

test("allowsOrbitCommand: destructive / externally-visible tiers still prompt", () => {
for (const sub of ["done", "prune", "clone", "config", "new"]) {
test("allowsOrbitCommand: framework-neutral lifecycle subcommands are not bundled", () => {
// done/new are non-destructive and reversible, but orbit cannot judge
// *when* running them is right — workflow timing is the user's call, so
// the framework takes no position: not bundled into the allow set, not
// marked must-confirm. Users who want them prompt-less allowlist them in
// their own agent settings.
assert.equal(allowsOrbitCommand("orbit done"), false)
assert.equal(allowsOrbitCommand("orbit done --pr https://example.com/pr/1"), false)
assert.equal(allowsOrbitCommand("orbit new \"fix api\""), false)
})

test("allowsOrbitCommand: always-prompt tiers still prompt", () => {
for (const sub of ["prune", "clone", "config"]) {
assert.equal(allowsOrbitCommand(`orbit ${sub}`), false)
}
})
Expand Down
18 changes: 10 additions & 8 deletions .opencode-plugin/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,9 @@ const invokesOrbitCli = (cmd: string): boolean =>
return false
})

// Subcommands in tiers 1–2 (read-only + idempotent workspace-write) from
// skills/CONSTRAINTS.md. Excluded: done, prune, clone, config, new.
// Subcommands in the framework-verified auto-approve tiers from
// skills/CONSTRAINTS.md. Excluded: prune, clone, config (always prompt);
// done, new (framework-neutral — the user's own allowlist decides).
const SAFE_SUBCOMMANDS = new Set([
"repos", "info", "status", "context", "goal",
"jot", "memo", "add", "switch", "sync",
Expand All @@ -93,9 +94,9 @@ const SAFE_SUBCOMMANDS = new Set([

// Auto-approve decision for a single bash command line, mirroring
// hooks/auto-approve.sh (parity contract: docs/spec-hooks.md). Only a bare,
// un-chained orbit invocation whose subcommand is in the safe tiers is
// allowed. All matching is token-exact — never substring: `--forceful` or an
// `--force=x`-style spelling must not trip the destructive guard.
// un-chained orbit invocation whose subcommand is in the framework-verified
// tiers is allowed. All matching is token-exact — never substring:
// `--forceful` or an `--force=x`-style spelling must not trip the destructive guard.
const allowsOrbitCommand = (cmd: string): boolean => {
// Refuse anything with shell chaining/redirection/substitution.
if (/[;&|`$()><\n]/.test(cmd)) return false
Expand Down Expand Up @@ -204,9 +205,10 @@ const orbitPlugin = (async ({ client, $ }) => {

// ── PreToolUse/Bash equivalent ──────────────────────────────────────
// Auto-approves single, un-chained orbit invocations whose subcommand is
// in the two safe tiers. Destructive/externally-visible subcommands
// (done, prune, clone, config, new) and sync --force still prompt. The
// decision itself lives in allowsOrbitCommand (pure, test-covered).
// in the framework-verified tiers. prune/clone/config always prompt;
// done/new are framework-neutral (not bundled — the user's own allowlist
// decides); sync --force/--branch prompt. The decision itself lives in
// allowsOrbitCommand (pure, test-covered).
"permission.ask": async (input, output) => {
try {
if (input.type !== "bash") return
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ Hardens the destructive surface: `prune` and `sync --force`/`--branch` become ma
- Brief parser and status steering hardened. ([#23](https://github.com/orbcli/orbit/pull/23))
- Plugin install works on SSH-less machines — `try.sh` defaults to HTTPS. ([#24](https://github.com/orbcli/orbit/pull/24))
- OpenCode auto-approve matches `--force` token-exactly. ([#25](https://github.com/orbcli/orbit/pull/25))
- Auto-approve tier contract restated by where the judgment lives: framework-verified subcommands (read-only / destructive read / idempotent workspace-write) stay bundled; `done`/`new` are **framework-neutral** — workflow timing is the user's call, so they are neither bundled nor marked must-confirm (users who want them prompt-less allowlist them in their own agent settings; snippets in `skills/CONSTRAINTS.md`); `prune`/`clone`/`config` and `sync --force`/`--branch` keep prompting. No hook behavior change — the tier table, USAGE §17, spec-hooks and SKILL now match what the hooks already did, replacing the stale "done/new are destructive, human-initiated" classification.
- Bare `orbit goal` doc promises converged to reality: it is a write path (editor on a TTY, stdin set otherwise) and never had a read path — the read is `orbit context goal`. USAGE, SKILL (workflow + examples) and CONSTRAINTS no longer promise the bare read. The execution-location matrix in spec-commands also gained the missing `orbit config` row (runs anywhere in the project).
- Jot queue stores entries in `[jot "<repo>"]` subsections — names plain git-config keys can't hold (`my_repo`, `2048`) now jot and pop correctly.
- `orbit clone` rejects a URL whose basename violates the pool-name contract (e.g. `.github`), pointing at `--name`.
- Workspace/repo inference compares physical paths — commands work through symlinked cwds.
Expand Down
7 changes: 4 additions & 3 deletions USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,9 +270,10 @@ orbit status task-01 # Specify when at project root
View/set workspace goal:

```bash
orbit goal # Read
orbit context goal # Read
orbit goal "new goal" # Set/update
echo "new goal" | orbit goal # Set from stdin (pipe-friendly)
orbit goal # Modify interactively (editor on a TTY) — never a read
orbit goal --clear # Delete goal
```

Expand Down Expand Up @@ -561,9 +562,9 @@ orbit completion bash > /path/to/bash-completion/completions/orbit

## 17. Auto-approving safe commands

An orbit session runs read-only and idempotent subcommands (`context` / `repos` / `info` / `status`, plus workspace-writes like `add` / `memo` / `jot`) constantly, so per-command confirmation prompts add up. Those safe tiers can run without a prompt; destructive or externally-visible commands (`done` `prune` `clone` `config` `new`) always keep prompting.
An orbit session runs read-only and idempotent subcommands (`context` / `repos` / `info` / `status`, plus workspace-writes like `add` / `memo` / `jot`) constantly, so per-command confirmation prompts add up. Those framework-verified tiers can run without a prompt; destructive or externally-visible commands (`prune` `clone` `config`) always keep prompting. Workflow-timing commands (`done` `new`) are deliberately outside the framework's list — orbit takes no position on when they should run; allowlist them in your own agent settings if you want them prompt-less.

**Plugin users — nothing to do:** both plugins ship a `PreToolUse` hook that auto-approves exactly the safe subcommands and fails safe. **Skill-only / other agents:** add a static allowlist to your agent settings.
**Plugin users — nothing to do:** all four plugins ship an auto-approve hook that approves exactly the framework-verified subcommands and fails safe. **Skill-only / other agents:** add a static allowlist to your agent settings.

The exact command tiers, the ready-to-paste allowlist snippet, and the rationale for each tier all live in [`skills/CONSTRAINTS.md`](skills/CONSTRAINTS.md#permission-and-auto-execution-policy).

Expand Down
1 change: 1 addition & 0 deletions docs/spec-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ There is no standalone `orbit init` command. Commands that require `.repos/` (`c
| `orbit doctor` | ✓ | ✓ | ✓ |
| `orbit version` | ✓ | ✓ | ✓ |
| `orbit completion` | ✓ | ✓ | ✓ |
| `orbit config` | ✓ | ✓ | ✓ |

## Workspace and Repo Inference

Expand Down
8 changes: 5 additions & 3 deletions docs/spec-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,13 +128,15 @@ fallback (the agent runs bare `orbit context` itself).
`hooks/auto-approve.sh` (wired as `PreToolUse`/`Bash` for Claude/Qoder,
`PermissionRequest`/`Bash` for Codex via the exit-code wrapper) auto-approves
only a **single, un-chained** `orbit` invocation whose subcommand is in the
two safe tiers — the tier contract itself lives in
auto-approve tiers — the tier contract itself lives in
[skills/CONSTRAINTS.md](../skills/CONSTRAINTS.md#permission-and-auto-execution-policy):

- Refuses anything with shell chaining/redirection/substitution (`;` `&` `|`
`` ` `` `$(` `>` `<`, newline) — the normal confirmation prompt happens.
- Refuses non-orbit binaries and tier-3 subcommands (`done` `prune` `clone`
`config` `new`, and `sync --force` / `sync --branch`).
- Refuses non-orbit binaries, the always-prompt tiers (`prune` `clone`
`config`, and `sync --force` / `sync --branch`), and the framework-neutral
workflow-timing commands (`done` `new` — not bundled; the user's own
allowlist decides).
- Matching is **token-exact** (whitespace split), never substring: `sync
--forceful` must not trip the destructive guard. Each token is normalized
(quotes and backslashes stripped) before comparison, because `'--force'`,
Expand Down
13 changes: 8 additions & 5 deletions hooks/auto-approve.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
# Orbit PreToolUse hook — auto-approve safe orbit commands.
#
# Reduces confirmation prompts for the agent's high-frequency orbit calls.
# Only auto-approves read-only + idempotent-workspace-write subcommands; the
# destructive / externally-visible ones (done, prune, clone, config, new) still
# fall through to the normal confirmation flow.
# Only auto-approves framework-verified subcommands (read-only + idempotent
# workspace-write). done/new are framework-neutral (workflow timing — the
# user's own allowlist decides); prune/clone/config always prompt. Anything
# non-matching falls through to the normal confirmation flow.
#
# Contract: on a match, print a PreToolUse "allow" decision on stdout and exit 0.
# On anything else, print nothing and exit 0 (normal confirmation preserved).
Expand Down Expand Up @@ -45,8 +46,10 @@ rest=${trimmed#"$first"}
rest=${rest#"${rest%%[![:space:]]*}"}
subcmd=${rest%%[[:space:]]*}

# Auto-approve tier: read-only + idempotent workspace writes.
# Excluded (still prompt): done, prune, clone, config, new.
# Auto-approve tier: framework-verified only (read-only + idempotent
# workspace writes). Excluded: prune, clone, config (always prompt —
# destructive / shared-infrastructure); done, new (framework-neutral —
# workflow timing, the user's own allowlist decides).
case "$subcmd" in
repos|info|status|context|goal|jot|memo|add|switch|sync|version|doctor|completion) ;;
*) exit 0 ;;
Expand Down
Loading
Loading