opencode-swarm 6.45.1 → 6.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -601,6 +601,117 @@ Per-agent overrides:
601
601
 
602
602
  </details>
603
603
 
604
+ <details>
605
+ <summary><strong>File Authority (Per-Agent Write Permissions)</strong></summary>
606
+
607
+ Swarm enforces per-agent file write authority — each agent can only write to specific paths. By default, these rules are hardcoded, but you can override them via config.
608
+
609
+ ### Default Rules
610
+
611
+ | Agent | Can Write | Blocked | Zones |
612
+ |-------|-----------|---------|-------|
613
+ | `architect` | Everything (except plan files) | `.swarm/plan.md`, `.swarm/plan.json` | `generated` |
614
+ | `coder` | `src/`, `tests/`, `docs/`, `scripts/` | `.swarm/` (entire directory) | `generated`, `config` |
615
+ | `reviewer` | `.swarm/evidence/`, `.swarm/outputs/` | `src/`, `.swarm/plan.md`, `.swarm/plan.json` | `generated` |
616
+ | `test_engineer` | `tests/`, `.swarm/evidence/` | `src/`, `.swarm/plan.md`, `.swarm/plan.json` | `generated` |
617
+ | `explorer` | Read-only | Everything | — |
618
+ | `sme` | Read-only | Everything | — |
619
+ | `docs` | `docs/`, `.swarm/outputs/` | — | `generated` |
620
+ | `designer` | `docs/`, `.swarm/outputs/` | — | `generated` |
621
+ | `critic` | `.swarm/evidence/` | — | `generated` |
622
+
623
+ ### Prefixed Agents
624
+
625
+ Prefixed agents (e.g., `paid_coder`, `mega_reviewer`, `local_architect`) inherit defaults from their canonical base agent via `stripKnownSwarmPrefix`. The lookup order is:
626
+
627
+ 1. Exact match for the prefixed name (if explicitly defined in user config)
628
+ 2. Fall back to the canonical agent's defaults (e.g., `paid_coder` → `coder`)
629
+
630
+ ```json
631
+ {
632
+ "authority": {
633
+ "rules": {
634
+ "coder": { "allowedPrefix": ["src/", "lib/"] },
635
+ "paid_coder": { "allowedPrefix": ["vendor/", "plugins/"] }
636
+ }
637
+ }
638
+ }
639
+ ```
640
+
641
+ In this example, `paid_coder` gets its own explicit rule, while other prefixed coders (e.g., `mega_coder`) fall back to `coder`.
642
+
643
+ ### Runtime Enforcement
644
+
645
+ Architect direct writes are enforced at runtime via `toolBefore` hook. This tracks writes to source code paths outside `.swarm/` and protects `.swarm/plan.md` and `.swarm/plan.json` from direct modification.
646
+
647
+ ### Configuration
648
+
649
+ Override default rules in `.opencode/opencode-swarm.json`:
650
+
651
+ ```json
652
+ {
653
+ "authority": {
654
+ "enabled": true,
655
+ "rules": {
656
+ "coder": {
657
+ "allowedPrefix": ["src/", "lib/", "scripts/"],
658
+ "blockedPrefix": [".swarm/"],
659
+ "blockedZones": ["generated"]
660
+ },
661
+ "explorer": {
662
+ "readOnly": false,
663
+ "allowedPrefix": ["notes/", "scratch/"]
664
+ }
665
+ }
666
+ }
667
+ }
668
+ ```
669
+
670
+ ### Rule Fields
671
+
672
+ | Field | Type | Description |
673
+ |-------|------|-------------|
674
+ | `readOnly` | boolean | If `true`, agent cannot write anywhere |
675
+ | `blockedExact` | string[] | Exact file paths that are blocked |
676
+ | `blockedPrefix` | string[] | Path prefixes that are blocked (e.g., `.swarm/`) |
677
+ | `allowedPrefix` | string[] | Only these path prefixes are allowed. Omit to remove restriction; set `[]` to deny all |
678
+ | `blockedZones` | string[] | File zones to block: `production`, `test`, `config`, `generated`, `docs`, `build` |
679
+
680
+ ### Merge Behavior
681
+
682
+ - User rules **override** hardcoded defaults for the specified agent
683
+ - Scalar fields (`readOnly`) — user value replaces default
684
+ - Array fields (`blockedPrefix`, `allowedPrefix`, etc.) — user array **replaces** entirely (not merged)
685
+ - If a field is omitted in the user rule for a **known agent** (one with hardcoded defaults), the default value for that field is preserved
686
+ - If a field is omitted in the user rule for a **custom agent** (not in the defaults list), that field is `undefined` — there are no defaults to inherit
687
+ - `allowedPrefix: []` explicitly denies all writes; omitting `allowedPrefix` entirely means no allowlist restriction is applied (all paths are evaluated against blocklist rules only)
688
+ - Setting `enabled: false` ignores all custom rules and uses hardcoded defaults
689
+
690
+ ### Custom Agents
691
+
692
+ Custom agents (not in the defaults list) start with no rules. Their write authority depends entirely on what you configure:
693
+
694
+ - **Not in config at all** — agent is denied with `Unknown agent` (no rule exists; this is not the same as "blocked from all writes")
695
+ - **In config without `allowedPrefix`** — no allowlist restriction applies; only any `blockedPrefix`, `blockedZones`, or `readOnly` rules you explicitly set will enforce limits
696
+ - **In config with `allowedPrefix: []`** — all writes are denied
697
+
698
+ To safely restrict a custom agent, always set `allowedPrefix` explicitly:
699
+
700
+ ```json
701
+ {
702
+ "authority": {
703
+ "rules": {
704
+ "my_custom_agent": {
705
+ "allowedPrefix": ["plugins/", "extensions/"],
706
+ "blockedZones": ["generated"]
707
+ }
708
+ }
709
+ }
710
+ }
711
+ ```
712
+
713
+ </details>
714
+
604
715
  <details>
605
716
  <summary><strong>Context Budget Guard</strong></summary>
606
717
 
@@ -780,6 +891,16 @@ Config file location: `~/.config/opencode/opencode-swarm.json` (global) or `.ope
780
891
  "coder": { "max_tool_calls": 500 }
781
892
  }
782
893
  },
894
+ "authority": {
895
+ "enabled": true,
896
+ "rules": {
897
+ "coder": {
898
+ "allowedPrefix": ["src/", "lib/"],
899
+ "blockedPrefix": [".swarm/"],
900
+ "blockedZones": ["generated"]
901
+ }
902
+ }
903
+ },
783
904
  "review_passes": {
784
905
  "always_security_review": false,
785
906
  "security_globs": ["**/*auth*", "**/*crypto*", "**/*session*"]
package/dist/cli/index.js CHANGED
@@ -18649,6 +18649,17 @@ var CompactionConfigSchema = exports_external.object({
18649
18649
  emergencyThreshold: exports_external.number().min(1).max(99).default(80),
18650
18650
  preserveLastNTurns: exports_external.number().int().min(1).default(5)
18651
18651
  });
18652
+ var AgentAuthorityRuleSchema = exports_external.object({
18653
+ readOnly: exports_external.boolean().optional(),
18654
+ blockedExact: exports_external.array(exports_external.string()).optional(),
18655
+ blockedPrefix: exports_external.array(exports_external.string()).optional(),
18656
+ allowedPrefix: exports_external.array(exports_external.string()).optional(),
18657
+ blockedZones: exports_external.array(exports_external.enum(["production", "test", "config", "generated", "docs", "build"])).optional()
18658
+ });
18659
+ var AuthorityConfigSchema = exports_external.object({
18660
+ enabled: exports_external.boolean().default(true),
18661
+ rules: exports_external.record(exports_external.string(), AgentAuthorityRuleSchema).default({})
18662
+ });
18652
18663
  var PluginConfigSchema = exports_external.object({
18653
18664
  agents: exports_external.record(exports_external.string(), AgentOverrideConfigSchema).optional(),
18654
18665
  swarms: exports_external.record(exports_external.string(), SwarmConfigSchema).optional(),
@@ -18665,6 +18676,7 @@ var PluginConfigSchema = exports_external.object({
18665
18676
  watchdog: WatchdogConfigSchema.optional(),
18666
18677
  self_review: SelfReviewConfigSchema.optional(),
18667
18678
  tool_filter: ToolFilterConfigSchema.optional(),
18679
+ authority: AuthorityConfigSchema.optional(),
18668
18680
  plan_cursor: PlanCursorConfigSchema.optional(),
18669
18681
  evidence: EvidenceConfigSchema.optional(),
18670
18682
  summaries: SummaryConfigSchema.optional(),
@@ -8,6 +8,7 @@ export declare const EvidenceTypeSchema: z.ZodEnum<{
8
8
  quality_budget: "quality_budget";
9
9
  placeholder: "placeholder";
10
10
  test: "test";
11
+ build: "build";
11
12
  review: "review";
12
13
  approval: "approval";
13
14
  note: "note";
@@ -15,7 +16,6 @@ export declare const EvidenceTypeSchema: z.ZodEnum<{
15
16
  syntax: "syntax";
16
17
  sast: "sast";
17
18
  sbom: "sbom";
18
- build: "build";
19
19
  }>;
20
20
  export type EvidenceType = z.infer<typeof EvidenceTypeSchema>;
21
21
  export declare const EvidenceVerdictSchema: z.ZodEnum<{
@@ -34,6 +34,7 @@ export declare const BaseEvidenceSchema: z.ZodObject<{
34
34
  quality_budget: "quality_budget";
35
35
  placeholder: "placeholder";
36
36
  test: "test";
37
+ build: "build";
37
38
  review: "review";
38
39
  approval: "approval";
39
40
  note: "note";
@@ -41,7 +42,6 @@ export declare const BaseEvidenceSchema: z.ZodObject<{
41
42
  syntax: "syntax";
42
43
  sast: "sast";
43
44
  sbom: "sbom";
44
- build: "build";
45
45
  }>;
46
46
  timestamp: z.ZodString;
47
47
  agent: z.ZodString;
@@ -468,6 +468,39 @@ export declare const CompactionConfigSchema: z.ZodObject<{
468
468
  preserveLastNTurns: z.ZodDefault<z.ZodNumber>;
469
469
  }, z.core.$strip>;
470
470
  export type CompactionConfig = z.infer<typeof CompactionConfigSchema>;
471
+ export declare const AgentAuthorityRuleSchema: z.ZodObject<{
472
+ readOnly: z.ZodOptional<z.ZodBoolean>;
473
+ blockedExact: z.ZodOptional<z.ZodArray<z.ZodString>>;
474
+ blockedPrefix: z.ZodOptional<z.ZodArray<z.ZodString>>;
475
+ allowedPrefix: z.ZodOptional<z.ZodArray<z.ZodString>>;
476
+ blockedZones: z.ZodOptional<z.ZodArray<z.ZodEnum<{
477
+ docs: "docs";
478
+ production: "production";
479
+ test: "test";
480
+ config: "config";
481
+ generated: "generated";
482
+ build: "build";
483
+ }>>>;
484
+ }, z.core.$strip>;
485
+ export type AgentAuthorityRule = z.infer<typeof AgentAuthorityRuleSchema>;
486
+ export declare const AuthorityConfigSchema: z.ZodObject<{
487
+ enabled: z.ZodDefault<z.ZodBoolean>;
488
+ rules: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
489
+ readOnly: z.ZodOptional<z.ZodBoolean>;
490
+ blockedExact: z.ZodOptional<z.ZodArray<z.ZodString>>;
491
+ blockedPrefix: z.ZodOptional<z.ZodArray<z.ZodString>>;
492
+ allowedPrefix: z.ZodOptional<z.ZodArray<z.ZodString>>;
493
+ blockedZones: z.ZodOptional<z.ZodArray<z.ZodEnum<{
494
+ docs: "docs";
495
+ production: "production";
496
+ test: "test";
497
+ config: "config";
498
+ generated: "generated";
499
+ build: "build";
500
+ }>>>;
501
+ }, z.core.$strip>>>;
502
+ }, z.core.$strip>;
503
+ export type AuthorityConfig = z.infer<typeof AuthorityConfigSchema>;
471
504
  export declare const PluginConfigSchema: z.ZodObject<{
472
505
  agents: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
473
506
  model: z.ZodOptional<z.ZodString>;
@@ -627,6 +660,23 @@ export declare const PluginConfigSchema: z.ZodObject<{
627
660
  enabled: z.ZodDefault<z.ZodBoolean>;
628
661
  overrides: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
629
662
  }, z.core.$strip>>;
663
+ authority: z.ZodOptional<z.ZodObject<{
664
+ enabled: z.ZodDefault<z.ZodBoolean>;
665
+ rules: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
666
+ readOnly: z.ZodOptional<z.ZodBoolean>;
667
+ blockedExact: z.ZodOptional<z.ZodArray<z.ZodString>>;
668
+ blockedPrefix: z.ZodOptional<z.ZodArray<z.ZodString>>;
669
+ allowedPrefix: z.ZodOptional<z.ZodArray<z.ZodString>>;
670
+ blockedZones: z.ZodOptional<z.ZodArray<z.ZodEnum<{
671
+ docs: "docs";
672
+ production: "production";
673
+ test: "test";
674
+ config: "config";
675
+ generated: "generated";
676
+ build: "build";
677
+ }>>>;
678
+ }, z.core.$strip>>>;
679
+ }, z.core.$strip>>;
630
680
  plan_cursor: z.ZodOptional<z.ZodObject<{
631
681
  enabled: z.ZodDefault<z.ZodBoolean>;
632
682
  max_tokens: z.ZodDefault<z.ZodNumber>;
@@ -6,7 +6,7 @@
6
6
  * - Layer 1 (Soft Warning @ warning_threshold): Sets warning flag for messagesTransform to inject warning
7
7
  * - Layer 2 (Hard Block @ 100%): Throws error in toolBefore to block further calls, injects STOP message
8
8
  */
9
- import { type GuardrailsConfig } from '../config/schema';
9
+ import { type AuthorityConfig, type GuardrailsConfig } from '../config/schema';
10
10
  import { type FileZone } from '../context/zone-classifier';
11
11
  /**
12
12
  * Retrieves stored input args for a given callID.
@@ -34,7 +34,7 @@ export declare function deleteStoredInputArgs(callID: string): void;
34
34
  * @param config Guardrails configuration (optional)
35
35
  * @returns Tool before/after hooks and messages transform hook
36
36
  */
37
- export declare function createGuardrailsHooks(directory: string, directoryOrConfig?: string | GuardrailsConfig, config?: GuardrailsConfig): {
37
+ export declare function createGuardrailsHooks(directory: string, directoryOrConfig?: string | GuardrailsConfig, config?: GuardrailsConfig, authorityConfig?: AuthorityConfig): {
38
38
  toolBefore: (input: {
39
39
  tool: string;
40
40
  sessionID: string;
@@ -103,13 +103,22 @@ export declare function validateAndRecordAttestation(dir: string, findingId: str
103
103
  valid: false;
104
104
  reason: string;
105
105
  }>;
106
+ type AgentRule = {
107
+ readOnly?: boolean;
108
+ blockedExact?: string[];
109
+ blockedPrefix?: string[];
110
+ allowedPrefix?: string[];
111
+ blockedZones?: FileZone[];
112
+ };
113
+ export declare const DEFAULT_AGENT_AUTHORITY_RULES: Record<string, AgentRule>;
106
114
  /**
107
115
  * Checks whether the given agent is authorised to write to the given file path.
108
116
  */
109
- export declare function checkFileAuthority(agentName: string, filePath: string, cwd: string): {
117
+ export declare function checkFileAuthority(agentName: string, filePath: string, cwd: string, authorityConfig?: AuthorityConfig): {
110
118
  allowed: true;
111
119
  } | {
112
120
  allowed: false;
113
121
  reason: string;
114
122
  zone?: FileZone;
115
123
  };
124
+ export {};
@@ -6,7 +6,7 @@ export { createDelegationGateHook } from './delegation-gate';
6
6
  export { createDelegationSanitizerHook } from './delegation-sanitizer';
7
7
  export { createDelegationTrackerHook } from './delegation-tracker';
8
8
  export { extractCurrentPhase, extractCurrentPhaseFromPlan, extractCurrentTask, extractCurrentTaskFromPlan, extractDecisions, extractIncompleteTasks, extractIncompleteTasksFromPlan, extractPatterns, } from './extractors';
9
- export { createGuardrailsHooks } from './guardrails';
9
+ export { checkFileAuthority, createGuardrailsHooks, DEFAULT_AGENT_AUTHORITY_RULES, } from './guardrails';
10
10
  export { classifyMessage, classifyMessages, containsPlanContent, isDuplicateToolRead, isStaleError, isToolResult, MessagePriority, type MessagePriorityType, type MessageWithParts, } from './message-priority';
11
11
  export { consolidateSystemMessages } from './messages-transform';
12
12
  export { extractModelInfo, NATIVE_MODEL_LIMITS, PROVIDER_CAPS, resolveModelLimit, } from './model-limits';
package/dist/index.js CHANGED
@@ -14573,7 +14573,7 @@ function resolveGuardrailsConfig(config2, agentName) {
14573
14573
  };
14574
14574
  return resolved;
14575
14575
  }
14576
- var KNOWN_SWARM_PREFIXES, SEPARATORS, AgentOverrideConfigSchema, SwarmConfigSchema, HooksConfigSchema, ScoringWeightsSchema, DecisionDecaySchema, TokenRatiosSchema, ScoringConfigSchema, ContextBudgetConfigSchema, EvidenceConfigSchema, GateFeatureSchema, PlaceholderScanConfigSchema, QualityBudgetConfigSchema, GateConfigSchema, PipelineConfigSchema, PhaseCompleteConfigSchema, SummaryConfigSchema, ReviewPassesConfigSchema, AdversarialDetectionConfigSchema, AdversarialTestingConfigSchemaBase, AdversarialTestingConfigSchema, IntegrationAnalysisConfigSchema, DocsConfigSchema, UIReviewConfigSchema, CompactionAdvisoryConfigSchema, LintConfigSchema, SecretscanConfigSchema, GuardrailsProfileSchema, DEFAULT_AGENT_PROFILES, DEFAULT_ARCHITECT_PROFILE, GuardrailsConfigSchema, WatchdogConfigSchema, SelfReviewConfigSchema, ToolFilterConfigSchema, PlanCursorConfigSchema, CheckpointConfigSchema, AutomationModeSchema, AutomationCapabilitiesSchema, AutomationConfigSchemaBase, AutomationConfigSchema, KnowledgeConfigSchema, CuratorConfigSchema, SlopDetectorConfigSchema, IncrementalVerifyConfigSchema, CompactionConfigSchema, PluginConfigSchema;
14576
+ var KNOWN_SWARM_PREFIXES, SEPARATORS, AgentOverrideConfigSchema, SwarmConfigSchema, HooksConfigSchema, ScoringWeightsSchema, DecisionDecaySchema, TokenRatiosSchema, ScoringConfigSchema, ContextBudgetConfigSchema, EvidenceConfigSchema, GateFeatureSchema, PlaceholderScanConfigSchema, QualityBudgetConfigSchema, GateConfigSchema, PipelineConfigSchema, PhaseCompleteConfigSchema, SummaryConfigSchema, ReviewPassesConfigSchema, AdversarialDetectionConfigSchema, AdversarialTestingConfigSchemaBase, AdversarialTestingConfigSchema, IntegrationAnalysisConfigSchema, DocsConfigSchema, UIReviewConfigSchema, CompactionAdvisoryConfigSchema, LintConfigSchema, SecretscanConfigSchema, GuardrailsProfileSchema, DEFAULT_AGENT_PROFILES, DEFAULT_ARCHITECT_PROFILE, GuardrailsConfigSchema, WatchdogConfigSchema, SelfReviewConfigSchema, ToolFilterConfigSchema, PlanCursorConfigSchema, CheckpointConfigSchema, AutomationModeSchema, AutomationCapabilitiesSchema, AutomationConfigSchemaBase, AutomationConfigSchema, KnowledgeConfigSchema, CuratorConfigSchema, SlopDetectorConfigSchema, IncrementalVerifyConfigSchema, CompactionConfigSchema, AgentAuthorityRuleSchema, AuthorityConfigSchema, PluginConfigSchema;
14577
14577
  var init_schema = __esm(() => {
14578
14578
  init_zod();
14579
14579
  init_constants();
@@ -15047,6 +15047,17 @@ var init_schema = __esm(() => {
15047
15047
  emergencyThreshold: exports_external.number().min(1).max(99).default(80),
15048
15048
  preserveLastNTurns: exports_external.number().int().min(1).default(5)
15049
15049
  });
15050
+ AgentAuthorityRuleSchema = exports_external.object({
15051
+ readOnly: exports_external.boolean().optional(),
15052
+ blockedExact: exports_external.array(exports_external.string()).optional(),
15053
+ blockedPrefix: exports_external.array(exports_external.string()).optional(),
15054
+ allowedPrefix: exports_external.array(exports_external.string()).optional(),
15055
+ blockedZones: exports_external.array(exports_external.enum(["production", "test", "config", "generated", "docs", "build"])).optional()
15056
+ });
15057
+ AuthorityConfigSchema = exports_external.object({
15058
+ enabled: exports_external.boolean().default(true),
15059
+ rules: exports_external.record(exports_external.string(), AgentAuthorityRuleSchema).default({})
15060
+ });
15050
15061
  PluginConfigSchema = exports_external.object({
15051
15062
  agents: exports_external.record(exports_external.string(), AgentOverrideConfigSchema).optional(),
15052
15063
  swarms: exports_external.record(exports_external.string(), SwarmConfigSchema).optional(),
@@ -15063,6 +15074,7 @@ var init_schema = __esm(() => {
15063
15074
  watchdog: WatchdogConfigSchema.optional(),
15064
15075
  self_review: SelfReviewConfigSchema.optional(),
15065
15076
  tool_filter: ToolFilterConfigSchema.optional(),
15077
+ authority: AuthorityConfigSchema.optional(),
15066
15078
  plan_cursor: PlanCursorConfigSchema.optional(),
15067
15079
  evidence: EvidenceConfigSchema.optional(),
15068
15080
  summaries: SummaryConfigSchema.optional(),
@@ -53558,7 +53570,7 @@ function isInDeclaredScope(filePath, scopeEntries, cwd) {
53558
53570
  return rel.length > 0 && !rel.startsWith("..") && !path35.isAbsolute(rel);
53559
53571
  });
53560
53572
  }
53561
- function createGuardrailsHooks(directory, directoryOrConfig, config3) {
53573
+ function createGuardrailsHooks(directory, directoryOrConfig, config3, authorityConfig) {
53562
53574
  let guardrailsConfig;
53563
53575
  if (directory && typeof directory === "object" && "enabled" in directory) {
53564
53576
  console.warn("[guardrails] Legacy call without directory, falling back to process.cwd()");
@@ -53576,6 +53588,7 @@ function createGuardrailsHooks(directory, directoryOrConfig, config3) {
53576
53588
  messagesTransform: async () => {}
53577
53589
  };
53578
53590
  }
53591
+ const precomputedAuthorityRules = buildEffectiveRules(authorityConfig);
53579
53592
  const cfg = guardrailsConfig;
53580
53593
  const requiredQaGates = cfg.qa_gates?.required_tools ?? [
53581
53594
  "diff",
@@ -53670,7 +53683,7 @@ function createGuardrailsHooks(directory, directoryOrConfig, config3) {
53670
53683
  if (typeof delegTargetPath === "string" && delegTargetPath.length > 0) {
53671
53684
  const agentName = swarmState.activeAgent.get(sessionID) ?? "unknown";
53672
53685
  const cwd = effectiveDirectory;
53673
- const authorityCheck = checkFileAuthority(agentName, delegTargetPath, cwd);
53686
+ const authorityCheck = checkFileAuthorityWithRules(agentName, delegTargetPath, cwd, precomputedAuthorityRules);
53674
53687
  if (!authorityCheck.allowed) {
53675
53688
  throw new Error(`WRITE BLOCKED: Agent "${agentName}" is not authorised to write "${delegTargetPath}". Reason: ${authorityCheck.reason}`);
53676
53689
  }
@@ -53679,6 +53692,19 @@ function createGuardrailsHooks(directory, directoryOrConfig, config3) {
53679
53692
  }
53680
53693
  }
53681
53694
  }
53695
+ if (tool3 === "apply_patch" || tool3 === "patch") {
53696
+ const agentName = swarmState.activeAgent.get(sessionID) ?? "unknown";
53697
+ const cwd = effectiveDirectory;
53698
+ for (const p of extractPatchTargetPaths(tool3, args2)) {
53699
+ const authorityCheck = checkFileAuthorityWithRules(agentName, p, cwd, precomputedAuthorityRules);
53700
+ if (!authorityCheck.allowed) {
53701
+ throw new Error(`WRITE BLOCKED: Agent "${agentName}" is not authorised to write "${p}" (via patch). Reason: ${authorityCheck.reason}`);
53702
+ }
53703
+ if (!currentSession.modifiedFilesThisCoderTask.includes(p)) {
53704
+ currentSession.modifiedFilesThisCoderTask.push(p);
53705
+ }
53706
+ }
53707
+ }
53682
53708
  } else if (isArchitect(sessionID)) {
53683
53709
  const coderDelegArgs = args2;
53684
53710
  const rawSubagentType = coderDelegArgs?.subagent_type;
@@ -53747,6 +53773,55 @@ function createGuardrailsHooks(directory, directoryOrConfig, config3) {
53747
53773
  }
53748
53774
  }
53749
53775
  }
53776
+ function extractPatchTargetPaths(tool3, args2) {
53777
+ if (tool3 !== "apply_patch" && tool3 !== "patch")
53778
+ return [];
53779
+ const toolArgs = args2;
53780
+ const patchText = toolArgs?.input ?? toolArgs?.patch ?? (Array.isArray(toolArgs?.cmd) ? toolArgs.cmd[1] : undefined);
53781
+ if (typeof patchText !== "string")
53782
+ return [];
53783
+ if (patchText.length > 1e6) {
53784
+ throw new Error("WRITE BLOCKED: Patch payload exceeds 1 MB \u2014 authority cannot be verified for all modified paths. Split into smaller patches.");
53785
+ }
53786
+ const paths = new Set;
53787
+ const patchPathPattern = /\*\*\*\s+(?:Update|Add|Delete)\s+File:\s*(.+)/gi;
53788
+ const diffPathPattern = /\+\+\+\s+b\/(.+)/gm;
53789
+ const gitDiffPathPattern = /^diff --git a\/(.+?) b\/(.+?)$/gm;
53790
+ const minusPathPattern = /^---\s+a\/(.+)$/gm;
53791
+ const traditionalMinusPattern = /^---\s+([^\s].+?)(?:\t.*)?$/gm;
53792
+ const traditionalPlusPattern = /^\+\+\+\s+([^\s].+?)(?:\t.*)?$/gm;
53793
+ for (const match of patchText.matchAll(patchPathPattern))
53794
+ paths.add(match[1].trim());
53795
+ for (const match of patchText.matchAll(diffPathPattern)) {
53796
+ const p = match[1].trim();
53797
+ if (p !== "/dev/null")
53798
+ paths.add(p);
53799
+ }
53800
+ for (const match of patchText.matchAll(gitDiffPathPattern)) {
53801
+ const aPath = match[1].trim();
53802
+ const bPath = match[2].trim();
53803
+ if (aPath !== "/dev/null")
53804
+ paths.add(aPath);
53805
+ if (bPath !== "/dev/null")
53806
+ paths.add(bPath);
53807
+ }
53808
+ for (const match of patchText.matchAll(minusPathPattern)) {
53809
+ const p = match[1].trim();
53810
+ if (p !== "/dev/null")
53811
+ paths.add(p);
53812
+ }
53813
+ for (const match of patchText.matchAll(traditionalMinusPattern)) {
53814
+ const p = match[1].trim();
53815
+ if (p !== "/dev/null" && !p.startsWith("a/") && !p.startsWith("b/"))
53816
+ paths.add(p);
53817
+ }
53818
+ for (const match of patchText.matchAll(traditionalPlusPattern)) {
53819
+ const p = match[1].trim();
53820
+ if (p !== "/dev/null" && !p.startsWith("a/") && !p.startsWith("b/"))
53821
+ paths.add(p);
53822
+ }
53823
+ return Array.from(paths);
53824
+ }
53750
53825
  function handlePlanAndScopeProtection(sessionID, tool3, args2) {
53751
53826
  const toolArgs = args2;
53752
53827
  const targetPath = toolArgs?.filePath ?? toolArgs?.path ?? toolArgs?.file ?? toolArgs?.target;
@@ -53759,68 +53834,25 @@ function createGuardrailsHooks(directory, directoryOrConfig, config3) {
53759
53834
  }
53760
53835
  }
53761
53836
  if (!targetPath && (tool3 === "apply_patch" || tool3 === "patch")) {
53762
- const patchText = toolArgs?.input ?? toolArgs?.patch ?? (Array.isArray(toolArgs?.cmd) ? toolArgs.cmd[1] : undefined);
53763
- if (typeof patchText === "string" && patchText.length <= 1e6) {
53764
- const patchPathPattern = /\*\*\*\s+(?:Update|Add|Delete)\s+File:\s*(.+)/gi;
53765
- const diffPathPattern = /\+\+\+\s+b\/(.+)/gm;
53766
- const paths = new Set;
53767
- for (const match of patchText.matchAll(patchPathPattern)) {
53768
- paths.add(match[1].trim());
53769
- }
53770
- for (const match of patchText.matchAll(diffPathPattern)) {
53771
- const p = match[1].trim();
53772
- if (p !== "/dev/null")
53773
- paths.add(p);
53774
- }
53775
- const gitDiffPathPattern = /^diff --git a\/(.+?) b\/(.+?)$/gm;
53776
- for (const match of patchText.matchAll(gitDiffPathPattern)) {
53777
- const aPath = match[1].trim();
53778
- const bPath = match[2].trim();
53779
- if (aPath !== "/dev/null")
53780
- paths.add(aPath);
53781
- if (bPath !== "/dev/null")
53782
- paths.add(bPath);
53783
- }
53784
- const minusPathPattern = /^---\s+a\/(.+)$/gm;
53785
- for (const match of patchText.matchAll(minusPathPattern)) {
53786
- const p = match[1].trim();
53787
- if (p !== "/dev/null")
53788
- paths.add(p);
53789
- }
53790
- const traditionalMinusPattern = /^---\s+([^\s].+?)(?:\t.*)?$/gm;
53791
- const traditionalPlusPattern = /^\+\+\+\s+([^\s].+?)(?:\t.*)?$/gm;
53792
- for (const match of patchText.matchAll(traditionalMinusPattern)) {
53793
- const p = match[1].trim();
53794
- if (p !== "/dev/null" && !p.startsWith("a/") && !p.startsWith("b/")) {
53795
- paths.add(p);
53796
- }
53797
- }
53798
- for (const match of patchText.matchAll(traditionalPlusPattern)) {
53799
- const p = match[1].trim();
53800
- if (p !== "/dev/null" && !p.startsWith("a/") && !p.startsWith("b/")) {
53801
- paths.add(p);
53802
- }
53803
- }
53804
- for (const p of paths) {
53805
- const resolvedP = path35.resolve(effectiveDirectory, p);
53806
- const planMdPath = path35.resolve(effectiveDirectory, ".swarm", "plan.md").toLowerCase();
53807
- const planJsonPath = path35.resolve(effectiveDirectory, ".swarm", "plan.json").toLowerCase();
53808
- if (resolvedP.toLowerCase() === planMdPath || resolvedP.toLowerCase() === planJsonPath) {
53809
- throw new Error("PLAN STATE VIOLATION: Direct writes to .swarm/plan.md and .swarm/plan.json are blocked. " + "plan.md is auto-regenerated from plan.json by PlanSyncWorker. " + "Use update_task_status() to mark tasks complete, " + "phase_complete() for phase transitions, or " + "save_plan to create/restructure plans.");
53810
- }
53811
- if (isOutsideSwarmDir(p, effectiveDirectory) && (isSourceCodePath(p) || hasTraversalSegments(p))) {
53812
- const session = swarmState.agentSessions.get(sessionID);
53813
- if (session) {
53814
- session.architectWriteCount++;
53815
- warn("Architect direct code edit detected via apply_patch", {
53816
- tool: tool3,
53817
- sessionID,
53818
- targetPath: p,
53819
- writeCount: session.architectWriteCount
53820
- });
53821
- }
53822
- break;
53837
+ for (const p of extractPatchTargetPaths(tool3, args2)) {
53838
+ const resolvedP = path35.resolve(effectiveDirectory, p);
53839
+ const planMdPath = path35.resolve(effectiveDirectory, ".swarm", "plan.md").toLowerCase();
53840
+ const planJsonPath = path35.resolve(effectiveDirectory, ".swarm", "plan.json").toLowerCase();
53841
+ if (resolvedP.toLowerCase() === planMdPath || resolvedP.toLowerCase() === planJsonPath) {
53842
+ throw new Error("PLAN STATE VIOLATION: Direct writes to .swarm/plan.md and .swarm/plan.json are blocked. " + "plan.md is auto-regenerated from plan.json by PlanSyncWorker. " + "Use update_task_status() to mark tasks complete, " + "phase_complete() for phase transitions, or " + "save_plan to create/restructure plans.");
53843
+ }
53844
+ if (isOutsideSwarmDir(p, effectiveDirectory) && (isSourceCodePath(p) || hasTraversalSegments(p))) {
53845
+ const session = swarmState.agentSessions.get(sessionID);
53846
+ if (session) {
53847
+ session.architectWriteCount++;
53848
+ warn("Architect direct code edit detected via apply_patch", {
53849
+ tool: tool3,
53850
+ sessionID,
53851
+ targetPath: p,
53852
+ writeCount: session.architectWriteCount
53853
+ });
53823
53854
  }
53855
+ break;
53824
53856
  }
53825
53857
  }
53826
53858
  }
@@ -53917,6 +53949,24 @@ function createGuardrailsHooks(directory, directoryOrConfig, config3) {
53917
53949
  handleTestSuiteBlocking(input.tool, output.args);
53918
53950
  if (isArchitect(input.sessionID) && isWriteTool(input.tool)) {
53919
53951
  handlePlanAndScopeProtection(input.sessionID, input.tool, output.args);
53952
+ const toolArgs = output.args;
53953
+ const targetPath = toolArgs?.filePath ?? toolArgs?.path ?? toolArgs?.file ?? toolArgs?.target;
53954
+ if (typeof targetPath === "string" && targetPath.length > 0) {
53955
+ const agentName = swarmState.activeAgent.get(input.sessionID) ?? "architect";
53956
+ const authorityCheck = checkFileAuthorityWithRules(agentName, targetPath, effectiveDirectory, precomputedAuthorityRules);
53957
+ if (!authorityCheck.allowed) {
53958
+ throw new Error(`WRITE BLOCKED: Agent "${agentName}" is not authorised to write "${targetPath}". Reason: ${authorityCheck.reason}`);
53959
+ }
53960
+ }
53961
+ }
53962
+ if (input.tool === "apply_patch" || input.tool === "patch") {
53963
+ const agentName = swarmState.activeAgent.get(input.sessionID) ?? "architect";
53964
+ for (const p of extractPatchTargetPaths(input.tool, output.args)) {
53965
+ const authorityCheck = checkFileAuthorityWithRules(agentName, p, effectiveDirectory, precomputedAuthorityRules);
53966
+ if (!authorityCheck.allowed) {
53967
+ throw new Error(`WRITE BLOCKED: Agent "${agentName}" is not authorised to write "${p}" (via patch). Reason: ${authorityCheck.reason}`);
53968
+ }
53969
+ }
53920
53970
  }
53921
53971
  const resolved = resolveSessionAndWindow(input.sessionID);
53922
53972
  if (!resolved)
@@ -54411,7 +54461,7 @@ function hashArgs(args2) {
54411
54461
  return 0;
54412
54462
  }
54413
54463
  }
54414
- var AGENT_AUTHORITY_RULES = {
54464
+ var DEFAULT_AGENT_AUTHORITY_RULES = {
54415
54465
  architect: {
54416
54466
  blockedExact: [".swarm/plan.md", ".swarm/plan.json"],
54417
54467
  blockedZones: ["generated"]
@@ -54452,12 +54502,38 @@ var AGENT_AUTHORITY_RULES = {
54452
54502
  blockedZones: ["generated"]
54453
54503
  }
54454
54504
  };
54455
- function checkFileAuthority(agentName, filePath, cwd) {
54505
+ function buildEffectiveRules(authorityConfig) {
54506
+ if (authorityConfig?.enabled === false || !authorityConfig?.rules) {
54507
+ return DEFAULT_AGENT_AUTHORITY_RULES;
54508
+ }
54509
+ const entries = Object.entries(authorityConfig.rules);
54510
+ if (entries.length === 0) {
54511
+ return DEFAULT_AGENT_AUTHORITY_RULES;
54512
+ }
54513
+ const merged = {
54514
+ ...DEFAULT_AGENT_AUTHORITY_RULES
54515
+ };
54516
+ for (const [agent, userRule] of entries) {
54517
+ const normalizedRuleKey = agent.toLowerCase();
54518
+ const existing = merged[normalizedRuleKey] ?? {};
54519
+ merged[normalizedRuleKey] = {
54520
+ ...existing,
54521
+ ...userRule,
54522
+ blockedExact: userRule.blockedExact ?? existing.blockedExact,
54523
+ blockedPrefix: userRule.blockedPrefix ?? existing.blockedPrefix,
54524
+ allowedPrefix: userRule.allowedPrefix ?? existing.allowedPrefix,
54525
+ blockedZones: userRule.blockedZones ?? existing.blockedZones
54526
+ };
54527
+ }
54528
+ return merged;
54529
+ }
54530
+ function checkFileAuthorityWithRules(agentName, filePath, cwd, effectiveRules) {
54456
54531
  const normalizedAgent = agentName.toLowerCase();
54532
+ const strippedAgent = stripKnownSwarmPrefix(agentName).toLowerCase();
54457
54533
  const dir = cwd || process.cwd();
54458
54534
  const resolved = path35.resolve(dir, filePath);
54459
54535
  const normalizedPath = path35.relative(dir, resolved).replace(/\\/g, "/");
54460
- const rules = AGENT_AUTHORITY_RULES[normalizedAgent];
54536
+ const rules = effectiveRules[normalizedAgent] ?? effectiveRules[strippedAgent];
54461
54537
  if (!rules) {
54462
54538
  return { allowed: false, reason: `Unknown agent: ${agentName}` };
54463
54539
  }
@@ -54484,8 +54560,8 @@ function checkFileAuthority(agentName, filePath, cwd) {
54484
54560
  }
54485
54561
  }
54486
54562
  }
54487
- if (rules.allowedPrefix && rules.allowedPrefix.length > 0) {
54488
- const isAllowed = rules.allowedPrefix.some((prefix) => normalizedPath.startsWith(prefix));
54563
+ if (rules.allowedPrefix != null) {
54564
+ const isAllowed = rules.allowedPrefix.length > 0 ? rules.allowedPrefix.some((prefix) => normalizedPath.startsWith(prefix)) : false;
54489
54565
  if (!isAllowed) {
54490
54566
  return {
54491
54567
  allowed: false,
@@ -70013,7 +70089,8 @@ var OpenCodeSwarm = async (ctx) => {
70013
70089
  console.warn("");
70014
70090
  }
70015
70091
  const delegationHandler = createDelegationTrackerHook(config3, guardrailsConfig.enabled);
70016
- const guardrailsHooks = createGuardrailsHooks(ctx.directory, undefined, guardrailsConfig);
70092
+ const authorityConfig = AuthorityConfigSchema.parse(config3.authority ?? {});
70093
+ const guardrailsHooks = createGuardrailsHooks(ctx.directory, undefined, guardrailsConfig, authorityConfig);
70017
70094
  const watchdogConfig = WatchdogConfigSchema.parse(config3.watchdog ?? {});
70018
70095
  const advisoryInjector = (sessionId, message) => {
70019
70096
  const s = swarmState.agentSessions.get(sessionId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm",
3
- "version": "6.45.1",
3
+ "version": "6.46.0",
4
4
  "description": "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",