diff --git a/config/factory/activate-settings.sh b/config/factory/activate-settings.sh new file mode 100644 index 000000000..62a781210 --- /dev/null +++ b/config/factory/activate-settings.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash +# Merge managed Droid settings into Factory's app-owned settings file. +# Usage: activate-settings.sh +set -euo pipefail + +MANAGED_SETTINGS="$1" +JQ_BIN="$2" +FACTORY_DIR="${HOME}/.factory" +SETTINGS="${FACTORY_DIR}/settings.json" +TEMP_SETTINGS="" + +cleanup() { + if [ -n "$TEMP_SETTINGS" ]; then + rm -f "$TEMP_SETTINGS" + fi +} +trap cleanup EXIT + +mkdir -p "$FACTORY_DIR" +TEMP_SETTINGS="$(mktemp "$FACTORY_DIR/settings.json.XXXXXX")" + +if [ -f "$SETTINGS" ]; then + # The jq program intentionally contains literal $HOME for Factory to expand. + # shellcheck disable=SC2016 + "$JQ_BIN" --slurpfile managed "$MANAGED_SETTINGS" ' + def is_legacy_droid_command: + tostring | contains("droid-hook.sh"); + + def is_home_command($command; $relative): + ($command == ("$HOME/" + $relative)) + or (((env.HOME // "") != "") + and ($command == ((env.HOME // "") + "/" + $relative))); + + def is_managed_hook_command($matcher): + tostring as $command + | (($matcher == "Execute") + and (is_home_command($command; "dotfiles/config/shared/hooks/security.sh") + or is_home_command($command; "dotfiles/config/shared/hooks/block-git-push.sh") + or is_home_command($command; "dotfiles/config/shared/hooks/block-gh-settings.sh"))) + or (($matcher == "^(Edit|Write|Create|ApplyPatch)$") + and (is_home_command($command; "dotfiles/config/shared/hooks/secret-guard.sh") + or is_home_command($command; ".cargo/bin/git-ai checkpoint droid --hook-input stdin"))); + + def clean_hook_group: + if ((.hooks? | type) == "array") then + .matcher as $matcher + | .hooks |= map( + select( + (((.command? // "") | is_legacy_droid_command) | not) + and (((.command? // "") | is_managed_hook_command($matcher)) | not) + ) + ) + | select((.hooks | length) > 0) + else + . + end; + + ($managed[0]) as $managed_settings + | .sessionDefaultSettings = + ((.sessionDefaultSettings // {}) * $managed_settings.sessionDefaultSettings) + | .customModels = + ((.customModels // []) + | if type == "array" then + map(select(.id != $managed_settings.customModels[0].id)) + + $managed_settings.customModels + else + $managed_settings.customModels + end) + | .hooks = (.hooks // {}) + | .hooks |= with_entries( + if ((.value | type) == "array") then + .value |= map(clean_hook_group) + else + . + end + ) + | if ((.hooks.importedClaudeHooks? | type) == "array") then + .hooks.importedClaudeHooks |= map( + select(if type == "string" + then ((is_legacy_droid_command or contains("git-ai checkpoint droid")) | not) + else true + end) + ) + else + . + end + | reduce ($managed_settings.hooks | to_entries[]) as $entry (.; + .hooks[$entry.key] = + (((.hooks[$entry.key] // []) + | if type == "array" then map(clean_hook_group) else [] end) + + $entry.value) + ) + ' "$SETTINGS" >"$TEMP_SETTINGS" +else + # shellcheck disable=SC2016 + "$JQ_BIN" --slurpfile managed "$MANAGED_SETTINGS" -n ' + ($managed[0]) + ' >"$TEMP_SETTINGS" +fi + +mv -f "$TEMP_SETTINGS" "$SETTINGS" +chmod 600 "$SETTINGS" diff --git a/config/factory/default.nix b/config/factory/default.nix index e0f734800..12c81ae98 100644 --- a/config/factory/default.nix +++ b/config/factory/default.nix @@ -1,6 +1,24 @@ -_: { +{ + config, + lib, + pkgs, + ... +}: +let + factorySettingsActivation = ./activate-settings.sh; +in +{ home.file.".factory/config.json" = { source = ./config.json; force = true; }; + + # Factory owns settings.json and rewrites it as the app evolves. Merge only + # the managed Droid model and git-ai hooks so unrelated Factory preferences + # survive. + home.activation.factorySettings = config.lib.dag.entryAfter [ "writeBoundary" ] '' + $DRY_RUN_CMD ${pkgs.bash}/bin/bash "${factorySettingsActivation}" \ + "${./settings.json}" \ + "${pkgs.jq}/bin/jq" + ''; } diff --git a/config/factory/settings.json b/config/factory/settings.json new file mode 100644 index 000000000..43468bae4 --- /dev/null +++ b/config/factory/settings.json @@ -0,0 +1,64 @@ +{ + "sessionDefaultSettings": { + "model": "custom:gemma3:4b-0", + "reasoningEffort": "none" + }, + "customModels": [ + { + "model": "gemma3:4b", + "id": "custom:gemma3:4b-0", + "index": 0, + "baseUrl": "http://127.0.0.1:11434/v1", + "apiKey": "ollama", + "displayName": "Gemma3:4b", + "maxOutputTokens": 64000, + "noImageSupport": true, + "provider": "generic-chat-completion-api" + } + ], + "hooks": { + "PreToolUse": [ + { + "matcher": "Execute", + "hooks": [ + { + "command": "$HOME/dotfiles/config/shared/hooks/security.sh", + "type": "command" + }, + { + "command": "$HOME/dotfiles/config/shared/hooks/block-git-push.sh", + "type": "command" + }, + { + "command": "$HOME/dotfiles/config/shared/hooks/block-gh-settings.sh", + "type": "command" + } + ] + }, + { + "matcher": "^(Edit|Write|Create|ApplyPatch)$", + "hooks": [ + { + "command": "$HOME/dotfiles/config/shared/hooks/secret-guard.sh", + "type": "command" + }, + { + "command": "$HOME/.cargo/bin/git-ai checkpoint droid --hook-input stdin", + "type": "command" + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "^(Edit|Write|Create|ApplyPatch)$", + "hooks": [ + { + "command": "$HOME/.cargo/bin/git-ai checkpoint droid --hook-input stdin", + "type": "command" + } + ] + } + ] + } +} diff --git a/config/factory/settings.tpl.json b/config/factory/settings.tpl.json new file mode 100644 index 000000000..7a3467892 --- /dev/null +++ b/config/factory/settings.tpl.json @@ -0,0 +1,64 @@ +{ + "sessionDefaultSettings": { + "model": "custom:__GEMMA_LOCAL_NONDOT__-0", + "reasoningEffort": "none" + }, + "customModels": [ + { + "model": "__GEMMA_LOCAL__", + "id": "custom:__GEMMA_LOCAL_NONDOT__-0", + "index": 0, + "baseUrl": "http://127.0.0.1:11434/v1", + "apiKey": "ollama", + "displayName": "__GEMMA_LOCAL_PRETTY__", + "maxOutputTokens": 64000, + "noImageSupport": true, + "provider": "generic-chat-completion-api" + } + ], + "hooks": { + "PreToolUse": [ + { + "matcher": "Execute", + "hooks": [ + { + "command": "$HOME/dotfiles/config/shared/hooks/security.sh", + "type": "command" + }, + { + "command": "$HOME/dotfiles/config/shared/hooks/block-git-push.sh", + "type": "command" + }, + { + "command": "$HOME/dotfiles/config/shared/hooks/block-gh-settings.sh", + "type": "command" + } + ] + }, + { + "matcher": "^(Edit|Write|Create|ApplyPatch)$", + "hooks": [ + { + "command": "$HOME/dotfiles/config/shared/hooks/secret-guard.sh", + "type": "command" + }, + { + "command": "$HOME/.cargo/bin/git-ai checkpoint droid --hook-input stdin", + "type": "command" + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "^(Edit|Write|Create|ApplyPatch)$", + "hooks": [ + { + "command": "$HOME/.cargo/bin/git-ai checkpoint droid --hook-input stdin", + "type": "command" + } + ] + } + ] + } +} diff --git a/config/shared/hooks/README.md b/config/shared/hooks/README.md index 57347760c..bfc1c634e 100644 --- a/config/shared/hooks/README.md +++ b/config/shared/hooks/README.md @@ -10,6 +10,9 @@ Both hooks exit `0` when a command may proceed and exit `2` with a `BLOCKED by ...` diagnostic when it must stop. +Factory Droid uses the Execute matcher for shell commands and the standard edit +matcher for file writes with these shared guardrails. + ## Protected operations The push hook blocks explicit and implicit updates or deletions of `main`, `master`, and the cached remote default branch. It resolves upstream and push configuration, bulk pushes, force variants, and Git aliases without executing alias bodies. Direct pushes remain allowed for `shunkakinoki/wiki` and `shunkakinoki/gthq`. diff --git a/config/shared/hooks/block-gh-settings.sh b/config/shared/hooks/block-gh-settings.sh index 0c1d62c87..7469e86ab 100755 --- a/config/shared/hooks/block-gh-settings.sh +++ b/config/shared/hooks/block-gh-settings.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Shared agent guardrail for GitHub repository control-plane mutations. +# Shared Codex/Claude/Cursor/Factory Droid agent guardrail for GitHub repository control-plane mutations. # This is an early warning only; restricted credentials and server-side # rulesets are the authoritative enforcement boundary. diff --git a/config/shared/hooks/block-git-push.sh b/config/shared/hooks/block-git-push.sh index e3eb5a5eb..fcf65c921 100755 --- a/config/shared/hooks/block-git-push.sh +++ b/config/shared/hooks/block-git-push.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Shared agent guardrail for pushes to protected default branches. +# Shared Codex/Claude/Cursor/Factory Droid agent guardrail for pushes to protected default branches. # This is an early warning only; remote rulesets and restricted credentials are # the authoritative enforcement boundary. diff --git a/config/shared/hooks/secret-guard.sh b/config/shared/hooks/secret-guard.sh index 10e954cd9..dd3df7435 100755 --- a/config/shared/hooks/secret-guard.sh +++ b/config/shared/hooks/secret-guard.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Shared PreToolUse hook for Claude Code and Codex Write/Edit operations. +# Shared PreToolUse hook for Claude Code, Codex, and Factory Droid Write/Edit operations. # Blocks writes containing secrets detected by gitleaks. # See: https://zenn.dev/takna/articles/secret-leak-prevention-4-layer set -euo pipefail diff --git a/config/shared/hooks/security.sh b/config/shared/hooks/security.sh index f840cff1e..43ce40312 100755 --- a/config/shared/hooks/security.sh +++ b/config/shared/hooks/security.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Shared Codex/Copilot/Cursor/Grok Security Hook +# Shared Codex/Copilot/Cursor/Factory Droid/Grok Security Hook # Blocks dangerous Bash commands by checking against deny patterns. # Returns exit code 2 to block, exit code 0 to allow. # @@ -26,7 +26,7 @@ input=$(cat) # Cursor sends only .command without a tool_name → empty passes through. tool_name=$(echo "$input" | jq -r '.tool.name // .tool_name // .toolName // empty' 2>/dev/null) case "$tool_name" in -"" | Bash | bash | shell) ;; +"" | Bash | bash | Execute | execute | shell) ;; *) exit 0 ;; esac diff --git a/models.json b/models.json index 5775c1798..1ccffad42 100644 --- a/models.json +++ b/models.json @@ -16,6 +16,7 @@ "glm": "glm-4.7", "minimax": "minimax-m3", "gemma": "gemma-4-31b-it", + "gemma-local": "gemma3:4b", "kimi": "kimi-k3", "qwen": "qwen3.6-plus", "qwen-local": "qwen3.5-0.8b-optiq" diff --git a/scripts/llm-update.sh b/scripts/llm-update.sh index 3a68d0f78..7faf5e0ca 100755 --- a/scripts/llm-update.sh +++ b/scripts/llm-update.sh @@ -96,6 +96,7 @@ declare -A TEMPLATES=( ["config/codex/config.tpl.toml"]=config/codex/config.toml ["config/cliproxyapi/config.tpl.yaml"]=config/cliproxyapi/config.template.yaml ["config/handy/settings_store.tpl.json"]=config/handy/settings_store.template.json + ["config/factory/settings.tpl.json"]=config/factory/settings.json ["config/hermes/config.tpl.yaml"]=config/hermes/config.template.yaml ["config/omp/config.tpl.yml"]=config/omp/config.yml ["config/pi/models.tpl.json"]=config/pi/models.json