Skip to content

Commit d95f5a6

Browse files
authored
Merge pull request #7 from BaseInfinity/v0.10.1-permission-sandboxes
v0.10.1: per-agent permission sandboxing
2 parents ac6b2a8 + a1ba982 commit d95f5a6

6 files changed

Lines changed: 301 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,86 @@
22

33
All notable changes to opencode-sdlc-wizard.
44

5+
## [0.10.1] - 2026-05-17
6+
7+
### Added — Per-agent permission sandboxing (`--sandbox-test-writer` + `--sandbox-docs`)
8+
9+
May-2026 community-patterns research: 9/15 surveyed `opencode.json`
10+
configurations use `agent.<name>.permission.write` to scope what each
11+
agent can touch. Most-cited patterns:
12+
13+
- `test-writer` locked to test/spec files only
14+
- `docs` locked to `.md` only
15+
16+
v0.10.1 exposes these as boolean flags on both `pick` and
17+
`configure-backend.sh`. Users wanting custom glob patterns still edit
18+
`opencode.json` directly.
19+
20+
```bash
21+
# Full v0.10.x hybrid in one shot — Mixed-Mode coder/reviewer split
22+
# plus sandboxed test-writer and docs agents
23+
npx opencode-sdlc-wizard pick \
24+
--tier proprietary --provider anthropic \
25+
--reviewer-tier hosted_oss --reviewer-provider cerebras \
26+
--sandbox-test-writer --sandbox-docs
27+
```
28+
29+
Yields:
30+
31+
```json
32+
{
33+
"model": "anthropic/claude-opus-4-7",
34+
"provider": {
35+
"anthropic": { "options": { "apiKey": "{env:ANTHROPIC_API_KEY}" } },
36+
"cerebras": { "npm": "@ai-sdk/openai-compatible", "options": { ... } }
37+
},
38+
"agent": {
39+
"review": { "model": "cerebras/gpt-oss-120b" },
40+
"test-writer": {
41+
"permission": { "write": {
42+
"**/*.test.*": "allow",
43+
"**/*.spec.*": "allow",
44+
"*": "deny"
45+
} }
46+
},
47+
"docs": {
48+
"permission": { "write": {
49+
"**/*.md": "allow",
50+
"*": "deny"
51+
} }
52+
}
53+
}
54+
}
55+
```
56+
57+
OpenCode enforces the permission patterns at write time — the
58+
test-writer agent can't accidentally clobber production source even
59+
if its model attempts to.
60+
61+
### Changed
62+
63+
- `scripts/configure-backend.sh`: new `--sandbox-test-writer` and
64+
`--sandbox-docs` boolean flags. The node heredoc deep-merges the
65+
canonical permission blocks alongside any v0.10.0 reviewer block,
66+
preserving user-set sibling fields.
67+
- `scripts/pick-backend.sh`: passes both flags through to the
68+
configurator unchanged.
69+
70+
### Tests
71+
72+
- `tests/test-backend-picker.sh` adds T35–T38:
73+
- T35: `--sandbox-test-writer` writes canonical `agent.test-writer.permission.write`
74+
- T36: `--sandbox-docs` writes canonical `agent.docs.permission.write`
75+
- T37: both sandboxes compose with `--reviewer-*` (full v0.10.x hybrid)
76+
- T38: no flags → no test-writer/docs agent blocks (opt-in regression guard)
77+
- `tests/test-pick.sh` adds T19–T22 (passthrough + composition + regression guard).
78+
- **349 tests across 12 suites** (was 341 / 12 in v0.10.0).
79+
80+
### Compat
81+
82+
- Opt-in only. Existing configs unchanged unless the new flags are passed.
83+
- Composes cleanly with v0.10.0 Mixed-Mode and v0.9.x single-model pick.
84+
585
## [0.10.0] - 2026-05-17
686

787
### Added — Mixed-Mode (per-agent model routing) in `pick` + `configure-backend.sh`

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "opencode-sdlc-wizard",
3-
"version": "0.10.0",
3+
"version": "0.10.1",
44
"description": "SDLC enforcement for OpenCode CLI — privacy-first, any-backend portability with a four-tier backend picker plus an OSS-tier cross-model-review skill so the full SDLC loop can run with zero Anthropic+OpenAI lock-in. Ships JSON Schemas for review artifacts so any consumer (cross-model-review, ditto, CI) can validate. Install with `npx opencode-sdlc-wizard init`. Sibling of agentic-sdlc-wizard and codex-sdlc-wizard.",
55
"bin": {
66
"opencode-sdlc-wizard": "cli/bin/opencode-sdlc-wizard.js"

scripts/configure-backend.sh

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ PRINT_ONLY=0
3333
REVIEWER_TIER=""
3434
REVIEWER_PROVIDER=""
3535
REVIEWER_MODEL=""
36+
# v0.10.1 Per-agent permission sandboxing. Boolean flags inject canonical
37+
# permission.write blocks for the two highest-signal agents from May-2026
38+
# community-patterns research (9/15 configs use this): test-writer scoped
39+
# to test/spec files only, docs scoped to .md only. Users wanting custom
40+
# glob patterns edit opencode.json directly.
41+
SANDBOX_TEST_WRITER=0
42+
SANDBOX_DOCS=0
3643

3744
usage() {
3845
sed -n '2,15p' "$0"
@@ -56,6 +63,8 @@ while [ $# -gt 0 ]; do
5663
--reviewer-provider=*) REVIEWER_PROVIDER="${1#*=}" ;;
5764
--reviewer-model) shift; REVIEWER_MODEL="${1:-}" ;;
5865
--reviewer-model=*) REVIEWER_MODEL="${1#*=}" ;;
66+
--sandbox-test-writer) SANDBOX_TEST_WRITER=1 ;;
67+
--sandbox-docs) SANDBOX_DOCS=1 ;;
5968
-h|--help) usage; exit 0 ;;
6069
*) echo "Unknown arg: $1" >&2; usage >&2; exit 2 ;;
6170
esac
@@ -84,15 +93,19 @@ CONFIG_PATH="$TARGET_DIR/opencode.json"
8493
# heredoc-fed node script so we don't have to escape JSON in bash, and so
8594
# the merge logic stays canonical (sorted keys, 2-space indent, trailing \n).
8695
node - "$TIER" "$PROVIDER" "$MODEL" "$CONFIG_PATH" "$FORCE" "$PRINT_ONLY" \
87-
"$REVIEWER_TIER" "$REVIEWER_PROVIDER" "$REVIEWER_MODEL" <<'NODE'
96+
"$REVIEWER_TIER" "$REVIEWER_PROVIDER" "$REVIEWER_MODEL" \
97+
"$SANDBOX_TEST_WRITER" "$SANDBOX_DOCS" <<'NODE'
8898
const fs = require("node:fs");
8999
const [
90100
tier, providerArg, model, configPath, forceStr, printOnlyStr,
91101
reviewerTier, reviewerProviderArg, reviewerModel,
102+
sandboxTestWriterStr, sandboxDocsStr,
92103
] = process.argv.slice(2);
93104
const force = forceStr === "1";
94105
const printOnly = printOnlyStr === "1";
95106
const mixedMode = Boolean(reviewerTier && reviewerProviderArg && reviewerModel);
107+
const sandboxTestWriter = sandboxTestWriterStr === "1";
108+
const sandboxDocs = sandboxDocsStr === "1";
96109
97110
// Canonical provider IDs. Detector emits user-friendly aliases; we accept both
98111
// and emit the canonical OpenCode/models.dev ID in the written config so model
@@ -399,6 +412,36 @@ if (mixedMode) {
399412
});
400413
}
401414
415+
// v0.10.1 Per-agent permission sandboxing. Each flag injects the canonical
416+
// permission.write pattern for that agent — deep-merged so it composes
417+
// with Mixed-Mode (--reviewer-*) and any user-set sibling fields. Patterns
418+
// derived from the most-cited community examples (joelhooks, ppries gists).
419+
if (sandboxTestWriter || sandboxDocs) {
420+
const sandboxAdditions = {};
421+
if (sandboxTestWriter) {
422+
sandboxAdditions["test-writer"] = {
423+
permission: {
424+
write: {
425+
"**/*.test.*": "allow",
426+
"**/*.spec.*": "allow",
427+
"*": "deny",
428+
},
429+
},
430+
};
431+
}
432+
if (sandboxDocs) {
433+
sandboxAdditions["docs"] = {
434+
permission: {
435+
write: {
436+
"**/*.md": "allow",
437+
"*": "deny",
438+
},
439+
},
440+
};
441+
}
442+
merged.agent = deepMerge(merged.agent || existing.agent || {}, sandboxAdditions);
443+
}
444+
402445
// Canonical key ordering for deterministic output (idempotency requirement).
403446
// Top-level: $schema, model, provider, then everything else alphabetical.
404447
function sortKeysCanonical(obj, topLevel = false) {

scripts/pick-backend.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ FREE_TIER_FIRST=0
5353
REVIEWER_TIER=""
5454
REVIEWER_PROVIDER=""
5555
REVIEWER_MODEL=""
56+
# v0.10.1 Per-agent permission sandboxing. Passthrough to configure-backend's
57+
# matching flags — canonical permission.write block per agent (test/spec
58+
# files for test-writer, .md only for docs).
59+
SANDBOX_TEST_WRITER=0
60+
SANDBOX_DOCS=0
5661

5762
while [ $# -gt 0 ]; do
5863
case "$1" in
@@ -73,6 +78,8 @@ while [ $# -gt 0 ]; do
7378
--reviewer-provider=*) REVIEWER_PROVIDER="${1#*=}" ;;
7479
--reviewer-model) shift; REVIEWER_MODEL="${1:-}" ;;
7580
--reviewer-model=*) REVIEWER_MODEL="${1#*=}" ;;
81+
--sandbox-test-writer) SANDBOX_TEST_WRITER=1 ;;
82+
--sandbox-docs) SANDBOX_DOCS=1 ;;
7683
-h|--help) usage; exit 0 ;;
7784
*) echo "Unknown arg: $1" >&2; usage >&2; exit 2 ;;
7885
esac
@@ -212,6 +219,8 @@ if [ -n "$REVIEWER_TIER" ]; then
212219
--reviewer-model "$REVIEWER_MODEL"
213220
)
214221
fi
222+
[ "$SANDBOX_TEST_WRITER" = "1" ] && CONFIGURE_ARGS+=(--sandbox-test-writer)
223+
[ "$SANDBOX_DOCS" = "1" ] && CONFIGURE_ARGS+=(--sandbox-docs)
215224

216225
if [ -n "$REVIEWER_TIER" ]; then
217226
echo "pick: resolved coder $TIER/$PROVIDER$MODEL + reviewer $REVIEWER_TIER/$REVIEWER_PROVIDER$REVIEWER_MODEL" >&2

tests/test-backend-picker.sh

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -716,6 +716,108 @@ console.log('ok');
716716
fi
717717
fi
718718

719+
# --- v0.10.1 Per-agent permission sandboxing. May-2026 community-patterns
720+
# research: 9/15 surveyed opencode.json files use agent.<name>.permission.write
721+
# to scope which paths each agent can touch — test-writer locked to test files,
722+
# docs locked to .md, etc. Maps directly onto the wizard's SDLC steps.
723+
# configure-backend exposes the two highest-signal sandboxes as boolean flags
724+
# that inject canonical permission blocks; users wanting custom glob patterns
725+
# edit opencode.json directly.
726+
727+
# --- T35: --sandbox-test-writer injects agent.test-writer.permission.write
728+
# restricting writes to test/spec files only
729+
if [ -x "$CONFIG" ]; then
730+
T="$TMP_ROOT/t35"; mkdir -p "$T"
731+
(cd "$T" && "$CONFIG" \
732+
--tier private_local --provider ollama --model qwen3-coder:30b \
733+
--sandbox-test-writer >/dev/null 2>&1) || true
734+
if [ -f "$T/opencode.json" ]; then
735+
ok="$(node -e "
736+
const j=require('$T/opencode.json');
737+
const tw=j.agent && j.agent['test-writer'];
738+
if(!tw||!tw.permission||!tw.permission.write){console.log('missing-test-writer-perm');process.exit(1)}
739+
const w=tw.permission.write;
740+
if(w['**/*.test.*']!=='allow'){console.log('missing-test-allow');process.exit(1)}
741+
if(w['**/*.spec.*']!=='allow'){console.log('missing-spec-allow');process.exit(1)}
742+
if(w['*']!=='deny'){console.log('missing-default-deny');process.exit(1)}
743+
console.log('ok');
744+
" 2>/dev/null || echo 'failed')"
745+
if [ "$ok" = "ok" ]; then
746+
pass "--sandbox-test-writer injects canonical permission.write block"
747+
else
748+
fail "T35 — $ok"
749+
fi
750+
fi
751+
fi
752+
753+
# --- T36: --sandbox-docs injects agent.docs.permission.write
754+
# restricting writes to .md only
755+
if [ -x "$CONFIG" ]; then
756+
T="$TMP_ROOT/t36"; mkdir -p "$T"
757+
(cd "$T" && "$CONFIG" \
758+
--tier private_local --provider ollama --model qwen3-coder:30b \
759+
--sandbox-docs >/dev/null 2>&1) || true
760+
if [ -f "$T/opencode.json" ]; then
761+
ok="$(node -e "
762+
const j=require('$T/opencode.json');
763+
const d=j.agent && j.agent.docs;
764+
if(!d||!d.permission||!d.permission.write){console.log('missing-docs-perm');process.exit(1)}
765+
const w=d.permission.write;
766+
if(w['**/*.md']!=='allow'){console.log('missing-md-allow');process.exit(1)}
767+
if(w['*']!=='deny'){console.log('missing-default-deny');process.exit(1)}
768+
console.log('ok');
769+
" 2>/dev/null || echo 'failed')"
770+
if [ "$ok" = "ok" ]; then
771+
pass "--sandbox-docs injects canonical permission.write block"
772+
else
773+
fail "T36 — $ok"
774+
fi
775+
fi
776+
fi
777+
778+
# --- T37: both sandboxes compose, plus Mixed-Mode reviewer — full v0.10.x
779+
# hybrid in one invocation
780+
if [ -x "$CONFIG" ]; then
781+
T="$TMP_ROOT/t37"; mkdir -p "$T"
782+
(cd "$T" && "$CONFIG" \
783+
--tier proprietary --provider anthropic --model claude-opus-4-7 \
784+
--reviewer-tier hosted_oss --reviewer-provider cerebras --reviewer-model gpt-oss-120b \
785+
--sandbox-test-writer --sandbox-docs >/dev/null 2>&1) || true
786+
if [ -f "$T/opencode.json" ]; then
787+
ok="$(node -e "
788+
const j=require('$T/opencode.json');
789+
if(j.agent.review.model!=='cerebras/gpt-oss-120b'){console.log('reviewer-wrong');process.exit(1)}
790+
if(!j.agent['test-writer'].permission.write['**/*.test.*']){console.log('missing-tw-sandbox');process.exit(1)}
791+
if(!j.agent.docs.permission.write['**/*.md']){console.log('missing-docs-sandbox');process.exit(1)}
792+
console.log('ok');
793+
" 2>/dev/null || echo 'failed')"
794+
if [ "$ok" = "ok" ]; then
795+
pass "v0.10.x hybrid: reviewer + test-writer sandbox + docs sandbox compose"
796+
else
797+
fail "T37 — $ok"
798+
fi
799+
fi
800+
fi
801+
802+
# --- T38: sandbox flags absent → no agent.test-writer / agent.docs blocks
803+
# (regression guard — sandbox is opt-in only)
804+
if [ -x "$CONFIG" ]; then
805+
T="$TMP_ROOT/t38"; mkdir -p "$T"
806+
(cd "$T" && "$CONFIG" --tier private_local --provider ollama --model qwen3-coder:30b >/dev/null 2>&1) || true
807+
if [ -f "$T/opencode.json" ]; then
808+
ok="$(node -e "
809+
const j=require('$T/opencode.json');
810+
if(j.agent && (j.agent['test-writer']||j.agent.docs)){console.log('unexpected-agent-block');process.exit(1)}
811+
console.log('ok');
812+
" 2>/dev/null || echo 'failed')"
813+
if [ "$ok" = "ok" ]; then
814+
pass "no sandbox flags → no test-writer/docs agent blocks (opt-in only)"
815+
else
816+
fail "T38 — $ok"
817+
fi
818+
fi
819+
fi
820+
719821
echo ""
720822
echo "=== Results: $PASS passed, $FAIL failed ==="
721823
[ "$FAIL" -eq 0 ] || exit 1

tests/test-pick.sh

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,71 @@ if [ -x "$SCRIPT" ]; then
334334
fi
335335
fi
336336

337+
# v0.10.1 sandbox passthrough — pick must forward --sandbox-test-writer and
338+
# --sandbox-docs to configure-backend without modification.
339+
340+
# T19: --sandbox-test-writer passthrough
341+
if [ -x "$SCRIPT" ]; then
342+
T="$TMP_ROOT/t19"; make_target "$T" "private_local/ollama"
343+
DETECT_STUB_LOG="$T/detect.log"
344+
CONFIGURE_STUB_LOG="$T/configure.log"
345+
(DETECT_STUB_LOG="$DETECT_STUB_LOG" CONFIGURE_STUB_LOG="$CONFIGURE_STUB_LOG" \
346+
PATH="$T/stubs:$PATH" "$SCRIPT" --sandbox-test-writer >/dev/null 2>&1) || true
347+
if [ -f "$CONFIGURE_STUB_LOG" ] && grep -q -- "--sandbox-test-writer" "$CONFIGURE_STUB_LOG"; then
348+
pass "--sandbox-test-writer passes through to configure-backend"
349+
else
350+
fail "--sandbox-test-writer was dropped"
351+
fi
352+
fi
353+
354+
# T20: --sandbox-docs passthrough
355+
if [ -x "$SCRIPT" ]; then
356+
T="$TMP_ROOT/t20"; make_target "$T" "private_local/ollama"
357+
DETECT_STUB_LOG="$T/detect.log"
358+
CONFIGURE_STUB_LOG="$T/configure.log"
359+
(DETECT_STUB_LOG="$DETECT_STUB_LOG" CONFIGURE_STUB_LOG="$CONFIGURE_STUB_LOG" \
360+
PATH="$T/stubs:$PATH" "$SCRIPT" --sandbox-docs >/dev/null 2>&1) || true
361+
if [ -f "$CONFIGURE_STUB_LOG" ] && grep -q -- "--sandbox-docs" "$CONFIGURE_STUB_LOG"; then
362+
pass "--sandbox-docs passes through to configure-backend"
363+
else
364+
fail "--sandbox-docs was dropped"
365+
fi
366+
fi
367+
368+
# T21: sandbox flags compose with Mixed-Mode reviewer in one pick invocation
369+
if [ -x "$SCRIPT" ]; then
370+
T="$TMP_ROOT/t21"; make_target "$T" "private_local/ollama"
371+
DETECT_STUB_LOG="$T/detect.log"
372+
CONFIGURE_STUB_LOG="$T/configure.log"
373+
(DETECT_STUB_LOG="$DETECT_STUB_LOG" CONFIGURE_STUB_LOG="$CONFIGURE_STUB_LOG" \
374+
PATH="$T/stubs:$PATH" "$SCRIPT" \
375+
--reviewer-tier hosted_oss --reviewer-provider cerebras \
376+
--sandbox-test-writer --sandbox-docs >/dev/null 2>&1) || true
377+
if [ -f "$CONFIGURE_STUB_LOG" ] \
378+
&& grep -q -- "--reviewer-provider cerebras" "$CONFIGURE_STUB_LOG" \
379+
&& grep -q -- "--sandbox-test-writer" "$CONFIGURE_STUB_LOG" \
380+
&& grep -q -- "--sandbox-docs" "$CONFIGURE_STUB_LOG"; then
381+
pass "v0.10.x hybrid: --reviewer-* + --sandbox-* compose in one pick call"
382+
else
383+
fail "compose failure — reviewer or sandbox args missing"
384+
cat "$CONFIGURE_STUB_LOG" 2>/dev/null | head -3 >&2 || true
385+
fi
386+
fi
387+
388+
# T22: no sandbox flags → no sandbox args forwarded (regression guard)
389+
if [ -x "$SCRIPT" ]; then
390+
T="$TMP_ROOT/t22"; make_target "$T" "private_local/ollama"
391+
DETECT_STUB_LOG="$T/detect.log"
392+
CONFIGURE_STUB_LOG="$T/configure.log"
393+
(DETECT_STUB_LOG="$DETECT_STUB_LOG" CONFIGURE_STUB_LOG="$CONFIGURE_STUB_LOG" \
394+
PATH="$T/stubs:$PATH" "$SCRIPT" >/dev/null 2>&1) || true
395+
if [ -f "$CONFIGURE_STUB_LOG" ] && ! grep -q -- "--sandbox-" "$CONFIGURE_STUB_LOG"; then
396+
pass "pick without --sandbox-* flags does NOT forward sandbox args"
397+
else
398+
fail "pick leaked --sandbox-* args to configure"
399+
fi
400+
fi
401+
337402
echo ""
338403
echo "=== Results: $PASS passed, $FAIL failed ==="
339404
[ "$FAIL" -eq 0 ] || exit 1

0 commit comments

Comments
 (0)