From 501858db72337e434651dd4f78f61ace1956c72e Mon Sep 17 00:00:00 2001 From: Sayuri Nekomiya Date: Sun, 3 May 2026 13:33:17 +1000 Subject: [PATCH 01/12] feat: added nix flake support --- .github/workflows/update-flake-lock.yml | 57 +++ flake.nix | 51 +++ nix/modules/home-manager/oac.nix | 517 ++++++++++++++++++++++++ 3 files changed, 625 insertions(+) create mode 100644 .github/workflows/update-flake-lock.yml create mode 100644 flake.nix create mode 100644 nix/modules/home-manager/oac.nix diff --git a/.github/workflows/update-flake-lock.yml b/.github/workflows/update-flake-lock.yml new file mode 100644 index 00000000..7c8d8f2d --- /dev/null +++ b/.github/workflows/update-flake-lock.yml @@ -0,0 +1,57 @@ +name: Update Flake Lock + +on: + schedule: + - cron: '0 3 * * 1' + workflow_dispatch: + +permissions: + contents: write + +concurrency: + group: update-flake-lock + cancel-in-progress: false + +jobs: + update-flake-lock: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@main + + - name: Update flake.lock + run: nix flake update + + - name: Validate flake + run: nix flake check + + - name: Test build + run: nix build .#default + + - name: Commit flake.lock changes + run: | + if git diff --quiet -- flake.lock; then + echo "No flake.lock changes detected" + exit 0 + fi + + git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git add flake.lock + git commit -m "chore(nix): update flake.lock [skip ci]" + git push + + - name: Summary + if: always() + run: | + echo "## Flake Lock Update" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- ✅ flake update attempted" >> $GITHUB_STEP_SUMMARY + echo "- ✅ flake check executed" >> $GITHUB_STEP_SUMMARY + echo "- ✅ build executed" >> $GITHUB_STEP_SUMMARY diff --git a/flake.nix b/flake.nix new file mode 100644 index 00000000..c165f2ca --- /dev/null +++ b/flake.nix @@ -0,0 +1,51 @@ +{ + description = "Nix flake module for OpenAgentsControl (OAC) + Home Manager OpenCode"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + home-manager = { + url = "github:nix-community/home-manager"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = + { + self, + nixpkgs, + ... + }: + let + systems = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; + + forEachSystem = + f: + builtins.listToAttrs ( + builtins.map (system: { + name = system; + value = f system; + }) systems + ); + in + { + homeManagerModules = { + oac = import ./nix/modules/home-manager/oac.nix { oacSource = self; }; + default = self.homeManagerModules.oac; + }; + + packages = forEachSystem ( + system: + let + pkgs = import nixpkgs { inherit system; }; + in + { + default = pkgs.writeText "oac-flake-module" "OpenAgentsControl Home Manager module"; + } + ); + }; +} diff --git a/nix/modules/home-manager/oac.nix b/nix/modules/home-manager/oac.nix new file mode 100644 index 00000000..88c8ff3f --- /dev/null +++ b/nix/modules/home-manager/oac.nix @@ -0,0 +1,517 @@ +{ oacSource ? null }: +{ lib, config, ... }: +let + inherit (lib) + mkEnableOption + mkIf + mkOption + mkDefault + types + nameValuePair + mapAttrs' + ; + + cfg = config.programs.opencode.oac; + + source = if cfg.source != null then cfg.source else oacSource; + + registry = + if source == null then + null + else + builtins.fromJSON (builtins.readFile "${source}/registry.json"); + + components = if registry == null then { } else registry.components; + contexts = components.contexts or [ ]; + profiles = if registry == null then { } else registry.profiles; + + parseSpec = + spec: + let + match = builtins.match "([^:]+):(.+)" spec; + in + if match == null then + null + else + { + type = builtins.elemAt match 0; + id = builtins.elemAt match 1; + }; + + hasWildcard = spec: builtins.match ".*\*.*" spec != null; + + toRegistryKey = + type: + if type == "agent" then + "agents" + else if type == "subagent" then + "subagents" + else if type == "command" then + "commands" + else if type == "tool" then + "tools" + else if type == "plugin" then + "plugins" + else if type == "skill" then + "skills" + else if type == "context" then + "contexts" + else if type == "config" then + "config" + else if lib.hasSuffix "s" type then + type + else + "${type}s"; + + findById = + items: id: + let + matches = builtins.filter (item: (item.id or null) == id || lib.elem id (item.aliases or [ ])) items; + in + if matches == [ ] then null else builtins.head matches; + + findContextByPathId = + id: + let + pathMd = ".opencode/context/${id}.md"; + pathAsIs = ".opencode/context/${id}"; + matches = builtins.filter (item: (item.path or "") == pathMd || (item.path or "") == pathAsIs) contexts; + in + if matches == [ ] then null else builtins.head matches; + + resolveComponent = + spec: + let + parsed = parseSpec spec; + in + if parsed == null then + null + else if parsed.type == "context" && lib.hasInfix "/" parsed.id then + findContextByPathId parsed.id + else + findById (components.${toRegistryKey parsed.type} or [ ]) parsed.id; + + stripMdSuffix = path: if lib.hasSuffix ".md" path then lib.removeSuffix ".md" path else path; + + expandContextPattern = + pattern: + let + match = builtins.match "(.*)\*.*" pattern; + prefixRaw = if match == null then pattern else builtins.elemAt match 0; + prefix = lib.removeSuffix "/" prefixRaw; + prefixWithSlash = if prefix == "" then "" else "${prefix}/"; + fullPrefix = ".opencode/context/${prefixWithSlash}"; + matches = builtins.filter (item: lib.hasPrefix fullPrefix (item.path or "")) contexts; + in + builtins.map ( + item: + "context:${stripMdSuffix (lib.removePrefix ".opencode/context/" item.path)}" + ) matches; + + expandSpec = + spec: + let + parsed = parseSpec spec; + in + if parsed == null then + [ ] + else if parsed.type == "context" && hasWildcard spec then + expandContextPattern parsed.id + else + [ spec ]; + + selectedProfile = + if cfg.profile == null then + null + else if builtins.hasAttr cfg.profile profiles then + builtins.getAttr cfg.profile profiles + else + null; + + profileComponents = if selectedProfile == null then [ ] else selectedProfile.components or [ ]; + profileAdditionalPaths = if selectedProfile == null then [ ] else selectedProfile.additionalPaths or [ ]; + + initialSpecs = lib.unique (lib.concatMap expandSpec (profileComponents ++ cfg.components)); + + dependenciesFor = + spec: + let + component = resolveComponent spec; + dependencies = if component == null then [ ] else component.dependencies or [ ]; + expandDependency = + dep: + let + parsed = parseSpec dep; + in + if parsed != null && parsed.type == "context" && hasWildcard dep then expandContextPattern parsed.id else [ dep ]; + in + lib.unique (lib.concatMap expandDependency dependencies); + + resolveAllDependencies = + seen: queue: + if queue == [ ] then + seen + else + let + current = builtins.head queue; + rest = builtins.tail queue; + in + if lib.elem current seen then + resolveAllDependencies seen rest + else + let + deps = dependenciesFor current; + in + resolveAllDependencies (seen ++ [ current ]) (deps ++ rest); + + resolvedSpecs = + if cfg.includeDependencies then + resolveAllDependencies [ ] initialSpecs + else + initialSpecs; + + finalSpecs = builtins.filter (spec: !(lib.elem spec cfg.excludeComponents)) resolvedSpecs; + + sourceFiles = + lib.unique ( + lib.concatMap ( + spec: + let + component = resolveComponent spec; + in + if component == null then [ ] else if component ? files then component.files else [ component.path ] + ) finalSpecs + ); + + layoutMap = { + agent = cfg.layout.agent; + command = cfg.layout.command; + context = cfg.layout.context; + tool = cfg.layout.tool; + plugin = cfg.layout.plugin; + skills = cfg.layout.skills; + config = cfg.layout.config; + }; + + mapRelativePath = + sourcePath: + if builtins.hasAttr sourcePath cfg.pathOverrides then + builtins.getAttr sourcePath cfg.pathOverrides + else if lib.hasPrefix ".opencode/" sourcePath then + let + rel = lib.removePrefix ".opencode/" sourcePath; + segments = lib.splitString "/" rel; + headSegment = builtins.head segments; + tailSegments = builtins.tail segments; + mappedHead = + if builtins.hasAttr headSegment layoutMap then builtins.getAttr headSegment layoutMap else headSegment; + mappedSegments = if mappedHead == "" then tailSegments else [ mappedHead ] ++ tailSegments; + in + lib.concatStringsSep "/" mappedSegments + else if cfg.layout.config == "" then + sourcePath + else + "${cfg.layout.config}/${sourcePath}"; + + withTargetRoot = + rel: + if cfg.targetRoot == "" then + rel + else + "${cfg.targetRoot}/${rel}"; + + contextReferencePath = + if cfg.contextReferencePath != null then cfg.contextReferencePath else "${config.xdg.configHome}/${withTargetRoot cfg.layout.context}"; + + rewriteContextRefs = + text: + if cfg.rewriteContextReferences then + builtins.replaceStrings + [ + "@.opencode/context/" + ".opencode/context" + ] + [ + "@${contextReferencePath}/" + contextReferencePath + ] + text + else + text; + + mkGeneratedFileEntry = + sourcePath: + let + src = "${source}/${sourcePath}"; + destRel = mapRelativePath sourcePath; + key = withTargetRoot destRel; + fileValue = + if cfg.rewriteContextReferences then + { + text = rewriteContextRefs (builtins.readFile src); + } + else + { + source = src; + }; + in + nameValuePair key ({ force = cfg.force; } // fileValue); + + generatedFileEntries = builtins.listToAttrs (builtins.map mkGeneratedFileEntry sourceFiles); + + additionalPaths = if cfg.installAdditionalPaths then profileAdditionalPaths else [ ]; + + mkAdditionalPathEntry = + relPath: + let + cleanRel = lib.removeSuffix "/" relPath; + src = "${source}/${cleanRel}"; + destRel = + if cfg.additionalPathsPrefix == "" then + cleanRel + else + "${cfg.additionalPathsPrefix}/${cleanRel}"; + key = withTargetRoot destRel; + base = { + source = src; + force = cfg.force; + }; + recursiveAttrs = if lib.pathIsDirectory src then { recursive = true; } else { }; + in + nameValuePair key (base // recursiveAttrs); + + additionalPathEntries = builtins.listToAttrs (builtins.map mkAdditionalPathEntry additionalPaths); + + mkUserEntry = + key: value: + nameValuePair + (withTargetRoot key) + ( + { force = cfg.force; } + // (if lib.isPath value then { source = value; } else { text = value; }) + ); + + extraFileEntries = mapAttrs' mkUserEntry cfg.extraFiles; + overrideEntries = mapAttrs' mkUserEntry cfg.overrides; + + missingFiles = builtins.filter (path: !(builtins.pathExists "${source}/${path}")) sourceFiles; +in +{ + options.programs.opencode.oac = { + enable = mkEnableOption "OpenAgentsControl profile installation for Home Manager OpenCode"; + + enableOpencode = mkOption { + type = types.bool; + default = true; + description = "Enable `programs.opencode` automatically when OAC is enabled."; + }; + + source = mkOption { + type = types.nullOr types.path; + default = null; + description = '' + OAC source tree containing `registry.json` and `.opencode/` files. + + By default this uses the source tree of this flake. + ''; + }; + + profile = mkOption { + type = types.nullOr (types.enum [ + "essential" + "developer" + "business" + "full" + "advanced" + ]); + default = "developer"; + description = '' + Profile to install from OAC `registry.json`. + + Set to `null` for custom-only component selection. + ''; + }; + + components = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ + "agent:openagent" + "command:add-context" + "context:core/*" + ]; + description = "Extra components to install on top of the selected profile."; + }; + + excludeComponents = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "plugin:notify" ]; + description = "Component specs to exclude after profile/dependency expansion."; + }; + + includeDependencies = mkOption { + type = types.bool; + default = true; + description = "Resolve and include transitive dependencies from `registry.json`."; + }; + + targetRoot = mkOption { + type = types.str; + default = "opencode"; + description = '' + Base directory under `$XDG_CONFIG_HOME` where OAC files are installed. + + Example: `opencode` -> `$XDG_CONFIG_HOME/opencode/...` + ''; + }; + + layout = { + agent = mkOption { + type = types.str; + default = "agent"; + description = "Directory name for OAC agent files."; + }; + + command = mkOption { + type = types.str; + default = "command"; + description = "Directory name for OAC command files."; + }; + + context = mkOption { + type = types.str; + default = "context"; + description = "Directory name for OAC context files."; + }; + + tool = mkOption { + type = types.str; + default = "tool"; + description = "Directory name for OAC tools."; + }; + + plugin = mkOption { + type = types.str; + default = "plugin"; + description = "Directory name for OAC plugins."; + }; + + skills = mkOption { + type = types.str; + default = "skills"; + description = "Directory name for OAC skills."; + }; + + config = mkOption { + type = types.str; + default = ""; + description = '' + Directory name for config-root files that are not under `.opencode/` (for example `env.example`). + + Empty string keeps them at the target root. + ''; + }; + }; + + pathOverrides = mkOption { + type = types.attrsOf types.str; + default = { }; + example = { + ".opencode/agent/core/openagent.md" = "agents/openagent.md"; + }; + description = "Exact source-path to destination-path overrides for generated OAC files."; + }; + + rewriteContextReferences = mkOption { + type = types.bool; + default = true; + description = '' + Rewrite `.opencode/context` references in installed file content to a global config path, + mirroring the installer's global path rewrite behavior. + ''; + }; + + contextReferencePath = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Explicit path used for rewritten context references. + + When null, defaults to `${config.xdg.configHome}//`. + ''; + }; + + installAdditionalPaths = mkOption { + type = types.bool; + default = false; + description = '' + Install profile `additionalPaths` entries (currently used by `advanced`) as recursive xdg config files. + + The install script currently reports these as manual downloads. + ''; + }; + + additionalPathsPrefix = mkOption { + type = types.str; + default = "additional"; + description = "Destination prefix under `targetRoot` for profile `additionalPaths` when enabled."; + }; + + force = mkOption { + type = types.bool; + default = false; + description = "Set `xdg.configFile..force` for generated OAC files."; + }; + + extraFiles = mkOption { + type = types.attrsOf (types.either types.lines types.path); + default = { }; + example = { + "context/project-intelligence/technical-domain.md" = ./technical-domain.md; + }; + description = "Additional files installed under `targetRoot` after generated profile files."; + }; + + overrides = mkOption { + type = types.attrsOf (types.either types.lines types.path); + default = { }; + example = { + "agent/core/openagent.md" = ./openagent.md; + }; + description = "Final override files/text installed under `targetRoot` (applied last)."; + }; + }; + + config = mkIf cfg.enable { + assertions = [ + { + assertion = source != null; + message = "programs.opencode.oac.source is null and no default oacSource was provided by the flake module."; + } + { + assertion = builtins.pathExists "${source}/registry.json"; + message = "OAC source does not contain registry.json."; + } + { + assertion = missingFiles == [ ]; + message = + "Some resolved OAC files were not found in source: " + + lib.concatStringsSep ", " missingFiles; + } + ]; + + warnings = + lib.optional + ( + cfg.profile == "advanced" + && profileAdditionalPaths != [ ] + && !cfg.installAdditionalPaths + ) + "programs.opencode.oac.profile=advanced includes additionalPaths in registry.json, but installAdditionalPaths=false so they are skipped (matching install.sh behavior)."; + + programs.opencode.enable = mkIf cfg.enableOpencode (mkDefault true); + + xdg.configFile = generatedFileEntries // additionalPathEntries // extraFileEntries // overrideEntries; + }; +} From 908061a50dafd799cd164b4e5dce7c39cb91786c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 3 May 2026 03:42:11 +0000 Subject: [PATCH 02/12] chore(nix): update flake.lock [skip ci] --- flake.lock | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 flake.lock diff --git a/flake.lock b/flake.lock new file mode 100644 index 00000000..81a2060d --- /dev/null +++ b/flake.lock @@ -0,0 +1,48 @@ +{ + "nodes": { + "home-manager": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1777771152, + "narHash": "sha256-+J0PGlJpLvo1ukmHM0ksI8PtBDwMCYyh87rbs86F0/c=", + "owner": "nix-community", + "repo": "home-manager", + "rev": "9c9fc9368a6d9e698d03d3780d3b930ebc060334", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "home-manager", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1777578337, + "narHash": "sha256-Ad49moKWeXtKBJNy2ebiTQUEgdLyvGmTeykAQ9xM+Z4=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "15f4ee454b1dce334612fa6843b3e05cf546efab", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "home-manager": "home-manager", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} From 4403d6d347b6d70795f44d7070c54f2f76f3d2b1 Mon Sep 17 00:00:00 2001 From: Sayuri Nekomiya Date: Sun, 3 May 2026 13:48:17 +1000 Subject: [PATCH 03/12] doc: added nix section in README.md --- README.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/README.md b/README.md index 77231544..0f67473d 100644 --- a/README.md +++ b/README.md @@ -205,6 +205,48 @@ Add a login endpoint --- +## For Nix Users: Installing With Flake Module + +If you use Nix, you can install OAC through this repository's built-in flake module. + +```nix +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + home-manager.url = "github:nix-community/home-manager"; + home-manager.inputs.nixpkgs.follows = "nixpkgs"; + + oac.url = "github:darrenhinde/OpenAgentsControl"; + }; + + outputs = { nixpkgs, home-manager, oac, ... }: { + homeConfigurations.my-user = home-manager.lib.homeManagerConfiguration { + pkgs = import nixpkgs { system = "x86_64-linux"; }; + modules = [ + oac.homeManagerModules.default + { + programs.opencode = { + enable = true; + oac = { + enable = true; + profile = "developer"; # essential|developer|business|full|advanced + }; + }; + } + ]; + }; + }; +} +``` + +Apply with Home Manager: + +```bash +home-manager switch --flake .#my-user +``` + +--- + ## 💡 The Context System: Your Secret Weapon **The problem with AI code:** It doesn't match your patterns. You spend hours refactoring. From 08c9b14b00b22b4cfc7f8eca4abf2a158178b80c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 06:09:37 +0000 Subject: [PATCH 04/12] chore(nix): update flake.lock [skip ci] --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 81a2060d..417bef2a 100644 --- a/flake.lock +++ b/flake.lock @@ -7,11 +7,11 @@ ] }, "locked": { - "lastModified": 1777771152, - "narHash": "sha256-+J0PGlJpLvo1ukmHM0ksI8PtBDwMCYyh87rbs86F0/c=", + "lastModified": 1777852249, + "narHash": "sha256-XdbGWnFlX4McOEG5NioVsp35Ic6XL/rXnp8as71cu6o=", "owner": "nix-community", "repo": "home-manager", - "rev": "9c9fc9368a6d9e698d03d3780d3b930ebc060334", + "rev": "c909892de502b4de9e92838a503c09a9c8ebe4aa", "type": "github" }, "original": { From ac2c9ed0e9f0f5566932289d1fd6c85e094820b3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 06:37:18 +0000 Subject: [PATCH 05/12] chore(nix): update flake.lock [skip ci] --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 417bef2a..9cd839a2 100644 --- a/flake.lock +++ b/flake.lock @@ -7,11 +7,11 @@ ] }, "locked": { - "lastModified": 1777852249, - "narHash": "sha256-XdbGWnFlX4McOEG5NioVsp35Ic6XL/rXnp8as71cu6o=", + "lastModified": 1778444552, + "narHash": "sha256-f18pIiR9q/p1vHY93gmAum7aHhQOG49oGvAB9+lptRo=", "owner": "nix-community", "repo": "home-manager", - "rev": "c909892de502b4de9e92838a503c09a9c8ebe4aa", + "rev": "dcebe66f958673729896eec2de4abfd86ef22d21", "type": "github" }, "original": { @@ -22,11 +22,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1777578337, - "narHash": "sha256-Ad49moKWeXtKBJNy2ebiTQUEgdLyvGmTeykAQ9xM+Z4=", + "lastModified": 1777954456, + "narHash": "sha256-hGdgeU2Nk87RAuZyYjyDjFL6LK7dAZN5RE9+hrDTkDU=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "15f4ee454b1dce334612fa6843b3e05cf546efab", + "rev": "549bd84d6279f9852cae6225e372cc67fb91a4c1", "type": "github" }, "original": { From 31b2e20a08a68a563ddd6ec2b83e81d0e396d31a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 06:55:02 +0000 Subject: [PATCH 06/12] chore(nix): update flake.lock [skip ci] --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 9cd839a2..9edd1332 100644 --- a/flake.lock +++ b/flake.lock @@ -7,11 +7,11 @@ ] }, "locked": { - "lastModified": 1778444552, - "narHash": "sha256-f18pIiR9q/p1vHY93gmAum7aHhQOG49oGvAB9+lptRo=", + "lastModified": 1779075347, + "narHash": "sha256-ZqttlFPw0meQMdABi8/vgle14OWQ3FkIqUkb4/Mrc+0=", "owner": "nix-community", "repo": "home-manager", - "rev": "dcebe66f958673729896eec2de4abfd86ef22d21", + "rev": "08c9d01457badce151aa4a983c475042d9dec018", "type": "github" }, "original": { @@ -22,11 +22,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1777954456, - "narHash": "sha256-hGdgeU2Nk87RAuZyYjyDjFL6LK7dAZN5RE9+hrDTkDU=", + "lastModified": 1778869304, + "narHash": "sha256-30sZNZoA1cqF5JNO9fVX+wgiQYjB7HJqqJ4ztCDeBZE=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "549bd84d6279f9852cae6225e372cc67fb91a4c1", + "rev": "d233902339c02a9c334e7e593de68855ad26c4cb", "type": "github" }, "original": { From 6e5432f563a3f336ca074602c3857a228e226d40 Mon Sep 17 00:00:00 2001 From: Sayuri Nekomiya Date: Mon, 18 May 2026 23:12:38 +1000 Subject: [PATCH 07/12] feat(nix): builtin permission directives; document and harden Home Manager OAC installation; fixes to soft links and missing navigations Add a dedicated Nix/Home Manager getting-started guide and wire it into the existing docs, plus upgrade the OAC HM module defaults and validation. The module now installs required bootstrap context files, defaults rewritten context references to the pinned flake source for reliable context discovery, and can emit safe builtin permission rules for OAC context + `.tmp` access with clear opt-in controls. This improves reproducible Nix onboarding while making default behavior safer and more explicit. --- README.md | 37 +-- docs/README.md | 7 +- docs/getting-started/installation.md | 2 + docs/getting-started/nix-home-manager.md | 336 +++++++++++++++++++++++ nix/modules/home-manager/oac.nix | 300 +++++++++++++++----- 5 files changed, 581 insertions(+), 101 deletions(-) create mode 100644 docs/getting-started/nix-home-manager.md diff --git a/README.md b/README.md index 0f67473d..31ce0ec2 100644 --- a/README.md +++ b/README.md @@ -205,45 +205,22 @@ Add a login endpoint --- -## For Nix Users: Installing With Flake Module +## For Nix Users -If you use Nix, you can install OAC through this repository's built-in flake module. +If you use Nix, OAC includes a Home Manager flake module: ```nix { - inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - home-manager.url = "github:nix-community/home-manager"; - home-manager.inputs.nixpkgs.follows = "nixpkgs"; - - oac.url = "github:darrenhinde/OpenAgentsControl"; - }; - - outputs = { nixpkgs, home-manager, oac, ... }: { - homeConfigurations.my-user = home-manager.lib.homeManagerConfiguration { - pkgs = import nixpkgs { system = "x86_64-linux"; }; - modules = [ - oac.homeManagerModules.default - { - programs.opencode = { - enable = true; - oac = { - enable = true; - profile = "developer"; # essential|developer|business|full|advanced - }; - }; - } - ]; - }; + programs.opencode.oac = { + enable = true; + profile = "developer"; }; } ``` -Apply with Home Manager: +Import `oac.homeManagerModules.default`, then apply with `home-manager switch --flake .#my-user`. -```bash -home-manager switch --flake .#my-user -``` +For the full setup, available `programs.opencode.oac.*` options, context reference behavior, bootstrap files, and built-in permission settings, see [docs/getting-started/nix-home-manager.md](docs/getting-started/nix-home-manager.md). --- diff --git a/docs/README.md b/docs/README.md index ba654a2c..dc21b1f9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,6 +8,7 @@ Welcome to the OpenAgents Control documentation! This directory contains all doc - **[Installation Guide](getting-started/installation.md)** - Complete installation guide with collision handling - **[Collision Handling](getting-started/collision-handling.md)** - Quick reference for install script collision strategies +- **[Nix Home Manager Guide](getting-started/nix-home-manager.md)** - Install OAC with the flake module and configure `programs.opencode.oac` ### Features @@ -50,7 +51,8 @@ docs/ ├── README.md # This file ├── getting-started/ │ ├── installation.md # Installation guide -│ └── collision-handling.md # Collision handling reference +│ ├── collision-handling.md # Collision handling reference +│ └── nix-home-manager.md # Nix/Home Manager flake module guide ├── features/ │ ├── system-builder/ │ │ ├── README.md # System builder quick start @@ -70,6 +72,9 @@ docs/ **...install OpenAgents Control** → [Installation Guide](getting-started/installation.md) +**...install OpenAgents Control with Nix/Home Manager** +→ [Nix Home Manager Guide](getting-started/nix-home-manager.md) + **...understand collision handling** → [Collision Handling](getting-started/collision-handling.md) diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 61949bef..62a6b01f 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -2,6 +2,8 @@ Complete guide to installing OpenAgents Control components using the automated installer script. +> Use Nix/Home Manager instead? See the [Nix Home Manager Guide](./nix-home-manager.md). + --- ## Quick Start diff --git a/docs/getting-started/nix-home-manager.md b/docs/getting-started/nix-home-manager.md new file mode 100644 index 00000000..2cb1cefc --- /dev/null +++ b/docs/getting-started/nix-home-manager.md @@ -0,0 +1,336 @@ +# OpenAgents Control with Nix Home Manager + +Use this guide if you want to install OpenAgents Control through the repository's built-in flake module instead of the shell installer. + +## When to use this + +This setup is a good fit if you already manage your OpenCode configuration with Nix and want OAC installation to be reproducible. + +If you do **not** already use Nix or Home Manager, the standard installer is simpler: + +- [Main README quick start](../../README.md#-quick-start) +- [Installation Guide](./installation.md) + +--- + +## Quick start + +Add OAC as a flake input and import its Home Manager module: + +```nix +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + home-manager.url = "github:nix-community/home-manager"; + home-manager.inputs.nixpkgs.follows = "nixpkgs"; + + oac.url = "github:darrenhinde/OpenAgentsControl"; + }; + + outputs = { nixpkgs, home-manager, oac, ... }: { + homeConfigurations.my-user = home-manager.lib.homeManagerConfiguration { + pkgs = import nixpkgs { system = "x86_64-linux"; }; + modules = [ + oac.homeManagerModules.default + { + programs.opencode.oac = { + enable = true; + profile = "developer"; + }; + } + ]; + }; + }; +} +``` + +Apply the configuration: + +```bash +home-manager switch --flake .#my-user +``` + +--- + +## What the module does by default + +When you enable `programs.opencode.oac`, the module: + +- imports `oac.homeManagerModules.default` +- enables `programs.opencode` automatically +- uses this flake's source tree by default +- rewrites `.opencode/context` references in installed files by default +- points rewritten context references at the pinned source-backed context path by default +- installs the bootstrap context files required for reliable context discovery +- enables a default set of OpenCode permission rules for OAC context access and project `.tmp` access + +--- + +## Common configuration + +### Minimal configuration + +```nix +{ + programs.opencode.oac = { + enable = true; + profile = "developer"; + }; +} +``` + +### Add extra components + +```nix +{ + programs.opencode.oac = { + enable = true; + profile = "developer"; + components = [ + "agent:openagent" + "command:add-context" + "context:core/*" + ]; + }; +} +``` + +### Install only custom-selected components + +Set `profile = null` to skip profile presets and install only the components you specify: + +```nix +{ + programs.opencode.oac = { + enable = true; + profile = null; + components = [ + "agent:openagent" + "command:add-context" + ]; + }; +} +``` + +### Exclude components from a profile + +```nix +{ + programs.opencode.oac = { + enable = true; + profile = "full"; + excludeComponents = [ + "plugin:notify" + ]; + }; +} +``` + +--- + +## Important options + +### Core options + +| Option | Default | What it does | +| --- | --- | --- | +| `enable` | `false` | Turns on OAC installation through Home Manager. | +| `enableOpencode` | `true` | Enables `programs.opencode` automatically when OAC is enabled. | +| `source` | flake source | Uses a custom OAC source tree containing `registry.json` and `.opencode/`. | +| `profile` | `"developer"` | Installs a preset profile: `essential`, `developer`, `business`, `full`, or `advanced`. Use `null` for custom-only selection. | +| `components` | `[]` | Adds extra registry components on top of the selected profile. | +| `excludeComponents` | `[]` | Removes specific components after profile and dependency expansion. | +| `includeDependencies` | `true` | Includes transitive dependencies from `registry.json`. | + +### Installation layout options + +| Option | Default | What it does | +| --- | --- | --- | +| `targetRoot` | `"opencode"` | Base directory under `$XDG_CONFIG_HOME` where files are installed. | +| `layout.agent` | `"agent"` | Destination directory name for agent files. | +| `layout.command` | `"command"` | Destination directory name for command files. | +| `layout.context` | `"context"` | Destination directory name for context files. | +| `layout.tool` | `"tool"` | Destination directory name for tool files. | +| `layout.plugin` | `"plugin"` | Destination directory name for plugins. | +| `layout.skills` | `"skills"` | Destination directory name for skills. | +| `layout.config` | `""` | Destination directory for files that are not under `.opencode/`. Empty keeps them at the target root. | +| `pathOverrides` | `{}` | Overrides exact generated destination paths for specific source files. | +| `force` | `false` | Sets `xdg.configFile..force` for generated files. | + +### Context and rewrite options + +| Option | Default | What it does | +| --- | --- | --- | +| `rewriteContextReferences` | `true` | Rewrites `.opencode/context` references inside installed files. | +| `contextReferencePath` | `null` | Overrides the path used for rewritten context references. | +| `extraFiles` | `{}` | Adds extra files under `targetRoot` after generated profile files. | +| `overrides` | `{}` | Replaces final installed files under `targetRoot` after all generation steps. | + +### Permission and advanced options + +| Option | Default | What it does | +| --- | --- | --- | +| `enableBuiltinPermissions` | `true` | Enables the module's built-in OpenCode permission rules. | +| `allowOacContextRead` | `true` | Allows reads to the OAC context reference path, denies edits there, and requires approval for matching bash commands. | +| `allowTmpDirFullAccess` | `true` | Allows reads, edits, and built-in `ls` / `mkdir` patterns for project `.tmp`. | +| `installAdditionalPaths` | `false` | Installs profile `additionalPaths` recursively as XDG config files. | +| `additionalPathsPrefix` | `"additional"` | Target prefix used when `installAdditionalPaths = true`. | + +--- + +## Bootstrap context files + +These bootstrap files are installed unless explicitly excluded: + +- `$XDG_CONFIG_HOME/opencode/context/navigation.md` +- `$XDG_CONFIG_HOME/opencode/context/core/config/paths.json` + +These are canonical discovery files and do **not** follow `pathOverrides`. + +--- + +## Context reference behavior + +By default, `contextReferencePath = null` resolves to the pinned source-backed context directory: + +```nix +{ + programs.opencode.oac.contextReferencePath = null; +} +``` + +That means rewritten references point at the flake source / Nix store path for `.opencode/context`, not the Home Manager symlink tree. This avoids symlink traversal issues during context discovery. + +If you want rewritten references to point at your config directory instead: + +```nix +{ config, ... }: +{ + programs.opencode.oac.contextReferencePath = "${config.xdg.configHome}/opencode/context"; +} +``` + +If you want to keep installed file contents unchanged, disable rewriting: + +```nix +{ + programs.opencode.oac.rewriteContextReferences = false; +} +``` + +--- + +## Built-in permissions + +The module can install a default permission policy for OpenCode. + +### Default behavior + +- `enableBuiltinPermissions = true` +- `allowOacContextRead = true` +- `allowTmpDirFullAccess = true` + +This means: + +- the OAC context reference path is readable +- edits to that context reference path are denied +- bash commands targeting that path require approval +- project `.tmp` reads and edits are allowed +- built-in `ls` and `mkdir` patterns for `.tmp` are allowed + +### Example: customize permissions + +```nix +{ + programs.opencode.oac = { + enable = true; + enableBuiltinPermissions = true; + allowOacContextRead = true; + allowTmpDirFullAccess = false; + }; +} +``` + +### Example: disable module-managed permissions completely + +```nix +{ + programs.opencode.oac = { + enable = true; + enableBuiltinPermissions = false; + }; +} +``` + +Use that if you want to manage OpenCode permissions yourself through `programs.opencode.settings`. + +--- + +## Path customization examples + +### Change the install root + +```nix +{ + programs.opencode.oac.targetRoot = "opencode-team"; +} +``` + +This installs files under `$XDG_CONFIG_HOME/opencode-team/...`. + +### Override a generated file path + +```nix +{ + programs.opencode.oac.pathOverrides = { + ".opencode/agent/core/openagent.md" = "agents/openagent.md"; + }; +} +``` + +### Add your own files after profile generation + +```nix +{ + programs.opencode.oac.extraFiles = { + "context/project-intelligence/technical-domain.md" = ./technical-domain.md; + }; +} +``` + +### Replace a generated file entirely + +```nix +{ + programs.opencode.oac.overrides = { + "agent/core/openagent.md" = ./openagent.md; + }; +} +``` + +--- + +## Notes about the `advanced` profile + +The `advanced` profile can reference `additionalPaths` entries from `registry.json`. + +Those are **not** installed by default, which matches the current shell installer behavior. To include them: + +```nix +{ + programs.opencode.oac = { + enable = true; + profile = "advanced"; + installAdditionalPaths = true; + }; +} +``` + +--- + +## Related docs + +- [Main README](../../README.md) +- [Installation Guide](./installation.md) +- [Documentation Index](../README.md) +- [OpenCode CLI Docs](https://opencode.ai/docs) diff --git a/nix/modules/home-manager/oac.nix b/nix/modules/home-manager/oac.nix index 88c8ff3f..dd4aa037 100644 --- a/nix/modules/home-manager/oac.nix +++ b/nix/modules/home-manager/oac.nix @@ -1,9 +1,12 @@ -{ oacSource ? null }: +{ + oacSource ? null, +}: { lib, config, ... }: let inherit (lib) mkEnableOption mkIf + mkMerge mkOption mkDefault types @@ -16,10 +19,7 @@ let source = if cfg.source != null then cfg.source else oacSource; registry = - if source == null then - null - else - builtins.fromJSON (builtins.readFile "${source}/registry.json"); + if source == null then null else builtins.fromJSON (builtins.readFile "${source}/registry.json"); components = if registry == null then { } else registry.components; contexts = components.contexts or [ ]; @@ -66,7 +66,9 @@ let findById = items: id: let - matches = builtins.filter (item: (item.id or null) == id || lib.elem id (item.aliases or [ ])) items; + matches = builtins.filter ( + item: (item.id or null) == id || lib.elem id (item.aliases or [ ]) + ) items; in if matches == [ ] then null else builtins.head matches; @@ -75,7 +77,9 @@ let let pathMd = ".opencode/context/${id}.md"; pathAsIs = ".opencode/context/${id}"; - matches = builtins.filter (item: (item.path or "") == pathMd || (item.path or "") == pathAsIs) contexts; + matches = builtins.filter ( + item: (item.path or "") == pathMd || (item.path or "") == pathAsIs + ) contexts; in if matches == [ ] then null else builtins.head matches; @@ -103,10 +107,7 @@ let fullPrefix = ".opencode/context/${prefixWithSlash}"; matches = builtins.filter (item: lib.hasPrefix fullPrefix (item.path or "")) contexts; in - builtins.map ( - item: - "context:${stripMdSuffix (lib.removePrefix ".opencode/context/" item.path)}" - ) matches; + map (item: "context:${stripMdSuffix (lib.removePrefix ".opencode/context/" item.path)}") matches; expandSpec = spec: @@ -129,9 +130,17 @@ let null; profileComponents = if selectedProfile == null then [ ] else selectedProfile.components or [ ]; - profileAdditionalPaths = if selectedProfile == null then [ ] else selectedProfile.additionalPaths or [ ]; + profileAdditionalPaths = + if selectedProfile == null then [ ] else selectedProfile.additionalPaths or [ ]; - initialSpecs = lib.unique (lib.concatMap expandSpec (profileComponents ++ cfg.components)); + bootstrapContextSpecs = [ + "context:root-navigation" + "context:context-paths-config" + ]; + + initialSpecs = lib.unique ( + lib.concatMap expandSpec (profileComponents ++ cfg.components ++ bootstrapContextSpecs) + ); dependenciesFor = spec: @@ -143,7 +152,10 @@ let let parsed = parseSpec dep; in - if parsed != null && parsed.type == "context" && hasWildcard dep then expandContextPattern parsed.id else [ dep ]; + if parsed != null && parsed.type == "context" && hasWildcard dep then + expandContextPattern parsed.id + else + [ dep ]; in lib.unique (lib.concatMap expandDependency dependencies); @@ -165,23 +177,53 @@ let resolveAllDependencies (seen ++ [ current ]) (deps ++ rest); resolvedSpecs = - if cfg.includeDependencies then - resolveAllDependencies [ ] initialSpecs - else - initialSpecs; + if cfg.includeDependencies then resolveAllDependencies [ ] initialSpecs else initialSpecs; finalSpecs = builtins.filter (spec: !(lib.elem spec cfg.excludeComponents)) resolvedSpecs; - sourceFiles = - lib.unique ( - lib.concatMap ( - spec: - let - component = resolveComponent spec; - in - if component == null then [ ] else if component ? files then component.files else [ component.path ] - ) finalSpecs - ); + requiredBootstrapSpecs = builtins.filter ( + spec: !(lib.elem spec cfg.excludeComponents) + ) bootstrapContextSpecs; + + bootstrapResolvedComponents = map (spec: { + inherit spec; + component = resolveComponent spec; + }) requiredBootstrapSpecs; + + missingBootstrapComponents = builtins.filter ( + entry: entry.component == null + ) bootstrapResolvedComponents; + + bootstrapExpectedSourceFiles = lib.unique ( + lib.concatMap ( + entry: + if entry.component == null then + [ ] + else if entry.component ? files then + entry.component.files + else + [ entry.component.path ] + ) bootstrapResolvedComponents + ); + + sourceFiles = lib.unique ( + lib.concatMap ( + spec: + let + component = resolveComponent spec; + in + if component == null then + [ ] + else if component ? files then + component.files + else + [ component.path ] + ) finalSpecs + ); + + missingBootstrapSourcePaths = builtins.filter ( + path: !(builtins.pathExists "${source}/${path}") + ) bootstrapExpectedSourceFiles; layoutMap = { agent = cfg.layout.agent; @@ -193,18 +235,19 @@ let config = cfg.layout.config; }; - mapRelativePath = + mapSourceRelativePath = sourcePath: - if builtins.hasAttr sourcePath cfg.pathOverrides then - builtins.getAttr sourcePath cfg.pathOverrides - else if lib.hasPrefix ".opencode/" sourcePath then + if lib.hasPrefix ".opencode/" sourcePath then let rel = lib.removePrefix ".opencode/" sourcePath; segments = lib.splitString "/" rel; headSegment = builtins.head segments; tailSegments = builtins.tail segments; mappedHead = - if builtins.hasAttr headSegment layoutMap then builtins.getAttr headSegment layoutMap else headSegment; + if builtins.hasAttr headSegment layoutMap then + builtins.getAttr headSegment layoutMap + else + headSegment; mappedSegments = if mappedHead == "" then tailSegments else [ mappedHead ] ++ tailSegments; in lib.concatStringsSep "/" mappedSegments @@ -213,15 +256,71 @@ let else "${cfg.layout.config}/${sourcePath}"; - withTargetRoot = - rel: - if cfg.targetRoot == "" then - rel + mapRelativePath = + sourcePath: + if builtins.hasAttr sourcePath cfg.pathOverrides then + builtins.getAttr sourcePath cfg.pathOverrides else - "${cfg.targetRoot}/${rel}"; + mapSourceRelativePath sourcePath; + + withTargetRoot = rel: if cfg.targetRoot == "" then rel else "${cfg.targetRoot}/${rel}"; contextReferencePath = - if cfg.contextReferencePath != null then cfg.contextReferencePath else "${config.xdg.configHome}/${withTargetRoot cfg.layout.context}"; + if cfg.contextReferencePath != null then + cfg.contextReferencePath + else + "${source}/.opencode/context"; + + contextReferencePathForPermissions = builtins.unsafeDiscardStringContext contextReferencePath; + + expandPermissionPaths = path: [ + path + "${path}/**" + ]; + + oacContextPermissionPaths = lib.unique ( + lib.concatMap expandPermissionPaths [ + contextReferencePathForPermissions + ] + ); + + oacContextBashPatterns = lib.unique [ + "* ${contextReferencePathForPermissions}*" + ]; + + tmpDirPermissionPaths = expandPermissionPaths ".tmp"; + + tmpDirBashPatterns = [ + "ls .tmp*" + "ls * .tmp*" + "ls \".tmp*" + "ls * \".tmp*" + "mkdir * tmp*" + "mkdir tmp*" + "mkdir \".tmp*" + "mkdir * \".tmp*" + ]; + + mkPermissionRules = + patterns: action: + builtins.listToAttrs (map (pattern: nameValuePair pattern (mkDefault action)) patterns); + + contextDirectoryPermissionSettings = { + permission = { + external_directory = mkPermissionRules oacContextPermissionPaths "allow"; + read = mkPermissionRules oacContextPermissionPaths "allow"; + edit = mkPermissionRules oacContextPermissionPaths "deny"; + bash = mkPermissionRules oacContextBashPatterns "ask"; + }; + }; + + tmpDirFullAccessSettings = { + permission = { + read = mkPermissionRules tmpDirPermissionPaths "allow"; + edit = mkPermissionRules tmpDirPermissionPaths "allow"; + bash = mkPermissionRules tmpDirBashPatterns "allow"; + }; + }; rewriteContextRefs = text: @@ -230,20 +329,22 @@ let [ "@.opencode/context/" ".opencode/context" + "~/.config/opencode/context" ] [ "@${contextReferencePath}/" contextReferencePath + contextReferencePath ] text else text; - mkGeneratedFileEntry = - sourcePath: + mkFileEntry = + pathMapper: sourcePath: let src = "${source}/${sourcePath}"; - destRel = mapRelativePath sourcePath; + destRel = pathMapper sourcePath; key = withTargetRoot destRel; fileValue = if cfg.rewriteContextReferences then @@ -257,7 +358,13 @@ let in nameValuePair key ({ force = cfg.force; } // fileValue); - generatedFileEntries = builtins.listToAttrs (builtins.map mkGeneratedFileEntry sourceFiles); + mkGeneratedFileEntry = mkFileEntry mapRelativePath; + + mkBootstrapFileEntry = mkFileEntry mapSourceRelativePath; + + generatedFileEntries = builtins.listToAttrs (map mkGeneratedFileEntry sourceFiles); + + bootstrapFileEntries = builtins.listToAttrs (map mkBootstrapFileEntry bootstrapExpectedSourceFiles); additionalPaths = if cfg.installAdditionalPaths then profileAdditionalPaths else [ ]; @@ -267,10 +374,7 @@ let cleanRel = lib.removeSuffix "/" relPath; src = "${source}/${cleanRel}"; destRel = - if cfg.additionalPathsPrefix == "" then - cleanRel - else - "${cfg.additionalPathsPrefix}/${cleanRel}"; + if cfg.additionalPathsPrefix == "" then cleanRel else "${cfg.additionalPathsPrefix}/${cleanRel}"; key = withTargetRoot destRel; base = { source = src; @@ -280,16 +384,13 @@ let in nameValuePair key (base // recursiveAttrs); - additionalPathEntries = builtins.listToAttrs (builtins.map mkAdditionalPathEntry additionalPaths); + additionalPathEntries = builtins.listToAttrs (map mkAdditionalPathEntry additionalPaths); mkUserEntry = key: value: - nameValuePair - (withTargetRoot key) - ( - { force = cfg.force; } - // (if lib.isPath value then { source = value; } else { text = value; }) - ); + nameValuePair (withTargetRoot key) ( + { force = cfg.force; } // (if lib.isPath value then { source = value; } else { text = value; }) + ); extraFileEntries = mapAttrs' mkUserEntry cfg.extraFiles; overrideEntries = mapAttrs' mkUserEntry cfg.overrides; @@ -306,6 +407,45 @@ in description = "Enable `programs.opencode` automatically when OAC is enabled."; }; + enableBuiltinPermissions = mkOption { + type = types.bool; + default = true; + description = '' + Enable OAC's built-in OpenCode permission rules. + + When enabled, this module can add the OAC context read policy and the project `.tmp` + access policy according to `allowOacContextRead` and `allowTmpDirFullAccess`. Set this + to `false` to disable all permission rules generated by this module while still installing + OAC files and preserving any permission settings you define directly in + `programs.opencode.settings`. + ''; + }; + + allowOacContextRead = mkOption { + type = types.bool; + default = true; + description = '' + Add default OpenCode permission rules for the OAC context reference path. + + This allows reads to the pinned OAC context path while denying edits and requiring + `ask` approval for bash commands that target that path. Set to `false` to disable + this policy. This option only has an effect when `enableBuiltinPermissions` is true. + ''; + }; + + allowTmpDirFullAccess = mkOption { + type = types.bool; + default = true; + description = '' + Add default OpenCode permission rules for the project `.tmp` directory. + + This allows reads and edits for both `.tmp` and `.tmp/**` and adds allow rules for the + built-in `ls` and `mkdir` command patterns used to inspect or create `.tmp` directories. + Set to `false` to disable this policy. This option only has an effect when + `enableBuiltinPermissions` is true. + ''; + }; + source = mkOption { type = types.nullOr types.path; default = null; @@ -317,13 +457,15 @@ in }; profile = mkOption { - type = types.nullOr (types.enum [ - "essential" - "developer" - "business" - "full" - "advanced" - ]); + type = types.nullOr ( + types.enum [ + "essential" + "developer" + "business" + "full" + "advanced" + ] + ); default = "developer"; description = '' Profile to install from OAC `registry.json`. @@ -438,7 +580,8 @@ in description = '' Explicit path used for rewritten context references. - When null, defaults to `${config.xdg.configHome}//`. + When null, defaults to the pinned OAC source context directory in the Nix store. + This keeps context discovery on immutable real paths instead of Home Manager symlinks. ''; }; @@ -496,22 +639,39 @@ in { assertion = missingFiles == [ ]; message = - "Some resolved OAC files were not found in source: " - + lib.concatStringsSep ", " missingFiles; + "Some resolved OAC files were not found in source: " + lib.concatStringsSep ", " missingFiles; + } + { + assertion = missingBootstrapComponents == [ ]; + message = + "Required bootstrap components could not be resolved from registry/source: " + + lib.concatStringsSep ", " (map (entry: entry.spec) missingBootstrapComponents); + } + { + assertion = missingBootstrapSourcePaths == [ ]; + message = + "Resolved bootstrap components reference source paths that do not exist: " + + lib.concatStringsSep ", " missingBootstrapSourcePaths; } ]; warnings = lib.optional - ( - cfg.profile == "advanced" - && profileAdditionalPaths != [ ] - && !cfg.installAdditionalPaths - ) + (cfg.profile == "advanced" && profileAdditionalPaths != [ ] && !cfg.installAdditionalPaths) "programs.opencode.oac.profile=advanced includes additionalPaths in registry.json, but installAdditionalPaths=false so they are skipped (matching install.sh behavior)."; programs.opencode.enable = mkIf cfg.enableOpencode (mkDefault true); - xdg.configFile = generatedFileEntries // additionalPathEntries // extraFileEntries // overrideEntries; + programs.opencode.settings = mkMerge [ + (mkIf (cfg.enableBuiltinPermissions && cfg.allowOacContextRead) contextDirectoryPermissionSettings) + (mkIf (cfg.enableBuiltinPermissions && cfg.allowTmpDirFullAccess) tmpDirFullAccessSettings) + ]; + + xdg.configFile = + generatedFileEntries + // bootstrapFileEntries + // additionalPathEntries + // extraFileEntries + // overrideEntries; }; } From cf9ceec02f08a5f74fe2fe7564320205257eb8e4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 13:17:57 +0000 Subject: [PATCH 08/12] chore(nix): update flake.lock [skip ci] --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 9edd1332..e1906182 100644 --- a/flake.lock +++ b/flake.lock @@ -7,11 +7,11 @@ ] }, "locked": { - "lastModified": 1779075347, - "narHash": "sha256-ZqttlFPw0meQMdABi8/vgle14OWQ3FkIqUkb4/Mrc+0=", + "lastModified": 1779103424, + "narHash": "sha256-hBYJz5jnRDjACPrwdD064zwMW+s5bdNlG/lNQipLhgM=", "owner": "nix-community", "repo": "home-manager", - "rev": "08c9d01457badce151aa4a983c475042d9dec018", + "rev": "dd71501fb7005264feb4de78444a2e1518cd4f66", "type": "github" }, "original": { From b95e1e90fdc39903cfb23c5756fd45404a7df866 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 25 May 2026 07:12:51 +0000 Subject: [PATCH 09/12] chore(nix): update flake.lock [skip ci] --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index e1906182..28bd5cee 100644 --- a/flake.lock +++ b/flake.lock @@ -7,11 +7,11 @@ ] }, "locked": { - "lastModified": 1779103424, - "narHash": "sha256-hBYJz5jnRDjACPrwdD064zwMW+s5bdNlG/lNQipLhgM=", + "lastModified": 1779678629, + "narHash": "sha256-gHcIFg0mm+KFsg7iZQt67kni3+qR5U3PhEC9P7vKlZ4=", "owner": "nix-community", "repo": "home-manager", - "rev": "dd71501fb7005264feb4de78444a2e1518cd4f66", + "rev": "612bbe3b405ad5f71d7bf9edecc04b678a061652", "type": "github" }, "original": { @@ -22,11 +22,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1778869304, - "narHash": "sha256-30sZNZoA1cqF5JNO9fVX+wgiQYjB7HJqqJ4ztCDeBZE=", + "lastModified": 1779508470, + "narHash": "sha256-Ap9KJX+5xHIn3bPIpfNgT6MEXdAECECwo4/rmlQD74M=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "d233902339c02a9c334e7e593de68855ad26c4cb", + "rev": "29916453413845e54a65b8a1cf996842300cd299", "type": "github" }, "original": { From aeba63e25af78d14fe6cb5eef9eda676c71af948 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 1 Jun 2026 08:32:38 +0000 Subject: [PATCH 10/12] chore(nix): update flake.lock [skip ci] --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 28bd5cee..e11cf371 100644 --- a/flake.lock +++ b/flake.lock @@ -7,11 +7,11 @@ ] }, "locked": { - "lastModified": 1779678629, - "narHash": "sha256-gHcIFg0mm+KFsg7iZQt67kni3+qR5U3PhEC9P7vKlZ4=", + "lastModified": 1780099287, + "narHash": "sha256-efIPwVGtIWIjWcznhaop6XN6HxnOL8800hF6CBNvlqQ=", "owner": "nix-community", "repo": "home-manager", - "rev": "612bbe3b405ad5f71d7bf9edecc04b678a061652", + "rev": "7d8127d308c3fb9664f7e643eec944be74ebb37d", "type": "github" }, "original": { @@ -22,11 +22,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1779508470, - "narHash": "sha256-Ap9KJX+5xHIn3bPIpfNgT6MEXdAECECwo4/rmlQD74M=", + "lastModified": 1779560665, + "narHash": "sha256-tpyBcxPpcQb8ukyNF7DoCwfSY3VPsxHoYwj00Cayv5o=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "29916453413845e54a65b8a1cf996842300cd299", + "rev": "64c08a7ca051951c8eae34e3e3cb1e202fe36786", "type": "github" }, "original": { From 5d06e05fc07dc10dc786b699eba26b4a24ad3b10 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 07:28:19 +0000 Subject: [PATCH 11/12] chore(nix): update flake.lock [skip ci] --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index e11cf371..f32aeb04 100644 --- a/flake.lock +++ b/flake.lock @@ -7,11 +7,11 @@ ] }, "locked": { - "lastModified": 1780099287, - "narHash": "sha256-efIPwVGtIWIjWcznhaop6XN6HxnOL8800hF6CBNvlqQ=", + "lastModified": 1780885330, + "narHash": "sha256-aMA5oAq2Iv467U9s8YOb50DYQT9w0WJbyWqwlzHuLMs=", "owner": "nix-community", "repo": "home-manager", - "rev": "7d8127d308c3fb9664f7e643eec944be74ebb37d", + "rev": "4fe95527cbe952713318ada8a4d122e1a6ab120f", "type": "github" }, "original": { @@ -22,11 +22,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1779560665, - "narHash": "sha256-tpyBcxPpcQb8ukyNF7DoCwfSY3VPsxHoYwj00Cayv5o=", + "lastModified": 1780749050, + "narHash": "sha256-3av0pIjlOWQ6rDbNOmpUSvbNnJkGORQKKjb4LtCZsIY=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "64c08a7ca051951c8eae34e3e3cb1e202fe36786", + "rev": "a799d3e3886da994fa307f817a6bc705ae538eeb", "type": "github" }, "original": { From 9c1e85328c6e660b1fbd2c148faac044edc7c285 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 08:59:24 +0000 Subject: [PATCH 12/12] chore(nix): update flake.lock [skip ci] --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index f32aeb04..dc819644 100644 --- a/flake.lock +++ b/flake.lock @@ -7,11 +7,11 @@ ] }, "locked": { - "lastModified": 1780885330, - "narHash": "sha256-aMA5oAq2Iv467U9s8YOb50DYQT9w0WJbyWqwlzHuLMs=", + "lastModified": 1781497404, + "narHash": "sha256-9GAF8sSsnkyCVCWkomXR0T+zdSxyUlfPt6neQidimdg=", "owner": "nix-community", "repo": "home-manager", - "rev": "4fe95527cbe952713318ada8a4d122e1a6ab120f", + "rev": "1285cd3d6882a9847f2d56ed5541b3350c8a6162", "type": "github" }, "original": { @@ -22,11 +22,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1780749050, - "narHash": "sha256-3av0pIjlOWQ6rDbNOmpUSvbNnJkGORQKKjb4LtCZsIY=", + "lastModified": 1781074563, + "narHash": "sha256-md8WlXOlfnIeHeOScMTTHFyf2d6iaTwPl2apR5EQ3P4=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "a799d3e3886da994fa307f817a6bc705ae538eeb", + "rev": "9ae611a455b90cf061d8f332b977e387bda8e1ca", "type": "github" }, "original": {