@@ -66,6 +66,14 @@ SANDBOX_PLAN=0
6666CODER_TEMP=" "
6767PLANNER_TEMP=" "
6868REVIEWER_TEMP=" "
69+ SECURITY_TEMP=" "
70+ # v0.11.2 security agent. Same triplet shape as reviewer/planner;
71+ # writes agent.security.model + security provider block. Plus the
72+ # matching --sandbox-security flag for tools.{write,edit,patch}=false.
73+ SECURITY_TIER=" "
74+ SECURITY_PROVIDER=" "
75+ SECURITY_MODEL=" "
76+ SANDBOX_SECURITY=0
6977
7078usage () {
7179 sed -n ' 2,15p' " $0 "
@@ -110,6 +118,15 @@ while [ $# -gt 0 ]; do
110118 --planner-temp=* ) PLANNER_TEMP=" ${1#* =} " ;;
111119 --reviewer-temp) shift ; REVIEWER_TEMP=" ${1:- } " ;;
112120 --reviewer-temp=* ) REVIEWER_TEMP=" ${1#* =} " ;;
121+ --security-temp) shift ; SECURITY_TEMP=" ${1:- } " ;;
122+ --security-temp=* ) SECURITY_TEMP=" ${1#* =} " ;;
123+ --security-tier) shift ; SECURITY_TIER=" ${1:- } " ;;
124+ --security-tier=* ) SECURITY_TIER=" ${1#* =} " ;;
125+ --security-provider) shift ; SECURITY_PROVIDER=" ${1:- } " ;;
126+ --security-provider=* ) SECURITY_PROVIDER=" ${1#* =} " ;;
127+ --security-model) shift ; SECURITY_MODEL=" ${1:- } " ;;
128+ --security-model=* ) SECURITY_MODEL=" ${1#* =} " ;;
129+ --sandbox-security) SANDBOX_SECURITY=1 ;;
113130 -h|--help) usage; exit 0 ;;
114131 * ) echo " Unknown arg: $1 " >&2 ; usage >&2 ; exit 2 ;;
115132 esac
@@ -142,6 +159,16 @@ if [ "$PL_SET" -ne 0 ] && [ "$PL_SET" -ne 3 ]; then
142159 exit 2
143160fi
144161
162+ # v0.11.2 security validation: same all-or-nothing rule.
163+ SC_SET=0
164+ [ -n " $SECURITY_TIER " ] && SC_SET=$(( SC_SET+ 1 ))
165+ [ -n " $SECURITY_PROVIDER " ] && SC_SET=$(( SC_SET+ 1 ))
166+ [ -n " $SECURITY_MODEL " ] && SC_SET=$(( SC_SET+ 1 ))
167+ if [ " $SC_SET " -ne 0 ] && [ " $SC_SET " -ne 3 ]; then
168+ echo " --security-tier / --security-provider / --security-model must all be set together (or none)" >&2
169+ exit 2
170+ fi
171+
145172# v0.10.4 small-model validation.
146173SM_SET=0
147174[ -n " $SMALL_TIER " ] && SM_SET=$(( SM_SET+ 1 ))
@@ -163,7 +190,9 @@ node - "$TIER" "$PROVIDER" "$MODEL" "$CONFIG_PATH" "$FORCE" "$PRINT_ONLY" \
163190 " $PLANNER_TIER " " $PLANNER_PROVIDER " " $PLANNER_MODEL " \
164191 " $SMALL_TIER " " $SMALL_PROVIDER " " $SMALL_MODEL " \
165192 " $SANDBOX_PLAN " \
166- " $CODER_TEMP " " $PLANNER_TEMP " " $REVIEWER_TEMP " << 'NODE '
193+ " $CODER_TEMP " " $PLANNER_TEMP " " $REVIEWER_TEMP " \
194+ " $SECURITY_TIER " " $SECURITY_PROVIDER " " $SECURITY_MODEL " \
195+ " $SECURITY_TEMP " " $SANDBOX_SECURITY " << 'NODE '
167196const fs = require("node:fs");
168197const [
169198 tier, providerArg, model, configPath, forceStr, printOnlyStr,
@@ -173,6 +202,8 @@ const [
173202 smallTier, smallProviderArg, smallModel,
174203 sandboxPlanStr,
175204 coderTempStr, plannerTempStr, reviewerTempStr,
205+ securityTier, securityProviderArg, securityModel,
206+ securityTempStr, sandboxSecurityStr,
176207] = process.argv.slice(2);
177208const force = forceStr === "1";
178209const printOnly = printOnlyStr === "1";
@@ -182,12 +213,15 @@ const sandboxDocs = sandboxDocsStr === "1";
182213const plannerMode = Boolean(plannerTier && plannerProviderArg && plannerModel);
183214const smallMode = Boolean(smallTier && smallProviderArg && smallModel);
184215const sandboxPlan = sandboxPlanStr === "1";
216+ const securityMode = Boolean(securityTier && securityProviderArg && securityModel);
217+ const sandboxSecurity = sandboxSecurityStr === "1";
185218// Parse temperatures only if non-empty; empty string means "not set"
186219// (no temperature field emitted). Numbers preserve as numbers in JSON.
187220function parseTemp(s) { return s === "" ? null : Number(s); }
188221const coderTemp = parseTemp(coderTempStr);
189222const plannerTemp = parseTemp(plannerTempStr);
190223const reviewerTemp = parseTemp(reviewerTempStr);
224+ const securityTemp = parseTemp(securityTempStr);
191225
192226// Canonical provider IDs. Detector emits user-friendly aliases; we accept both
193227// and emit the canonical OpenCode/models.dev ID in the written config so model
@@ -530,6 +564,18 @@ if (plannerMode) {
530564 });
531565}
532566
567+ // v0.11.2 Security mode: identical shape to planner. Writes
568+ // agent.security.model + security provider block. Joelhooks-pattern
569+ // security agent typically pairs with --sandbox-security below.
570+ if (securityMode) {
571+ const securityProvider = PROVIDER_ALIASES[securityProviderArg] || securityProviderArg;
572+ const securityFragment = fragmentFor(securityTier, securityProvider, securityModel);
573+ merged.provider = deepMerge(merged.provider, securityFragment.provider || {});
574+ merged.agent = deepMerge(merged.agent || existing.agent || {}, {
575+ security: { model: securityFragment.model },
576+ });
577+ }
578+
533579// v0.10.4 small_model: top-level field (not nested under agent). Distinct
534580// from agent.plan.model — small_model is OpenCode's cross-cutting hint
535581// for "use this when the call is cheap" (title generation, summary
@@ -549,7 +595,7 @@ if (smallMode) {
549595// — categorical tool denial, distinct shape from the path-scoped
550596// permission.write blocks above. Composes with v0.10.2 planner model
551597// (agent.plan ends up with both .model AND .tools when both flags set).
552- if (sandboxTestWriter || sandboxDocs || sandboxPlan) {
598+ if (sandboxTestWriter || sandboxDocs || sandboxPlan || sandboxSecurity ) {
553599 const sandboxAdditions = {};
554600 if (sandboxTestWriter) {
555601 sandboxAdditions["test-writer"] = {
@@ -577,18 +623,27 @@ if (sandboxTestWriter || sandboxDocs || sandboxPlan) {
577623 tools: { write: false, edit: false, patch: false },
578624 };
579625 }
626+ // v0.11.2: security agent denies the same tools as plan — read +
627+ // reason, never write. Joelhooks-pattern; matches the canonical
628+ // "security review can't accidentally apply a patch" guarantee.
629+ if (sandboxSecurity) {
630+ sandboxAdditions["security"] = {
631+ tools: { write: false, edit: false, patch: false },
632+ };
633+ }
580634 merged.agent = deepMerge(merged.agent || existing.agent || {}, sandboxAdditions);
581635}
582636
583637// v0.11.1 per-agent temperatures. Each non-null value sets the
584638// agent.<name>.temperature field; deep-merges with any existing model /
585639// tools / permission siblings on that agent. --coder-temp targets the
586640// `build` agent (OpenCode's default agent name for code generation).
587- if (coderTemp !== null || plannerTemp !== null || reviewerTemp !== null) {
641+ if (coderTemp !== null || plannerTemp !== null || reviewerTemp !== null || securityTemp !== null ) {
588642 const tempAdditions = {};
589643 if (coderTemp !== null) tempAdditions["build"] = { temperature: coderTemp };
590644 if (plannerTemp !== null) tempAdditions["plan"] = { temperature: plannerTemp };
591645 if (reviewerTemp !== null) tempAdditions["review"] = { temperature: reviewerTemp };
646+ if (securityTemp !== null) tempAdditions["security"] = { temperature: securityTemp };
592647 merged.agent = deepMerge(merged.agent || existing.agent || {}, tempAdditions);
593648}
594649
0 commit comments