Harden local provider command validation

ober

28105ae954e6b20f5f257708ceab8d17157a9fa5

diff --git a/GAPS.md b/GAPS.md
index c1f5556..176be5f 100644
--- a/GAPS.md
+++ b/GAPS.md
@@ -437,6 +437,10 @@ Acceptance criteria:
 - Warn or refuse group/world-writable provider executables.
 - Keep repo-local executable refusal.
 
+Status: implemented with canonical `realpath` checks, direct symlink refusal,
+group/world-writable mode refusal via fixed-argv `stat`, and fixture coverage
+for repo-local, symlinked, and unsafe-mode local/embedding providers.
+
 ## P2: Product and architecture parity
 
 ### G-050: Monolithic implementation
diff --git a/main-binary.ss b/main-binary.ss
index e086576..bb56d41 100644
--- a/main-binary.ss
+++ b/main-binary.ss
@@ -1017,14 +1017,62 @@
 (def (path-inside-root? path root)
   (list-prefix-same? (string-split (macos-private-var-path root) #\/)
                      (string-split (macos-private-var-path path) #\/)))
+(def (process-succeeds? args)
+  (try
+   (begin (run-process args) #t)
+   (catch (e) #f)))
+
+(def (process-output/default args fallback)
+  (let ([out (try-result (run-process args))])
+    (if (ok? out) (string-trim (unwrap out)) fallback)))
+
+(def (canonical-provider-path path)
+  (process-output/default (list "realpath" path) ""))
+
+(def (provider-type-text path)
+  (let ([mac-type (process-output/default (list "stat" "-f" "%HT" path) "")])
+    (if (string-empty? mac-type)
+        (process-output/default (list "stat" "-c" "%F" path) "")
+        mac-type)))
+
+(def (provider-symlink? path)
+  (let ([kind (down (provider-type-text path))])
+    (if (string-contains kind "symbolic") #t #f)))
+
+(def (provider-mode-text path)
+  (let ([mac-mode (process-output/default (list "stat" "-f" "%Lp" path) "")])
+    (if (string-empty? mac-mode)
+        (process-output/default (list "stat" "-c" "%a" path) "")
+        mac-mode)))
+
+(def (writable-mode-digit? n)
+  (or (= n 2) (= n 3) (= n 6) (= n 7)))
+
+(def (mode-group-or-world-writable? text)
+  (let* ([mode (parse-int text 0)]
+         [group (modulo (quotient mode 10) 10)]
+         [world (modulo mode 10)])
+    (or (writable-mode-digit? group)
+        (writable-mode-digit? world))))
+
+(def (provider-common-warning repo cmd missing absolute inside symlink writable missing-path)
+  (cond [(null? cmd) missing]
+        [(not (path-absolute? (car cmd))) absolute]
+        [(provider-symlink? (car cmd)) symlink]
+        [else
+         (let ([canonical (canonical-provider-path (car cmd))])
+           (cond [(string-empty? canonical) missing-path]
+                 [(path-inside-root? canonical repo) inside]
+                 [(mode-group-or-world-writable? (provider-mode-text canonical)) writable]
+                 [else #f]))]))
 (def (local-provider-command-warning repo cmd)
-  (cond [(null? cmd)
-         "local provider requested but no local_provider_command config or JERBOA_AIGIT_LOCAL_PROVIDER is set"]
-        [(not (path-absolute? (car cmd)))
-         "local provider command must be an absolute path outside the scanned repository"]
-        [(path-inside-root? (car cmd) repo)
-         "local provider command inside the scanned repository was refused"]
-        [else #f]))
+  (provider-common-warning repo cmd
+                           "local provider requested but no local_provider_command config or JERBOA_AIGIT_LOCAL_PROVIDER is set"
+                           "local provider command must be an absolute path outside the scanned repository"
+                           "local provider command inside the scanned repository was refused"
+                           "local provider command must be canonical and not a symlink"
+                           "local provider command is group/world-writable and was refused"
+                           "local provider command path does not exist or cannot be canonicalized"))
 (def (local-provider-result repo rev subject paths lines signals score)
   (cond [(not current-llm-requested?) (list '() '())]
         [(not (same-public-string? current-requested-provider "local"))
@@ -1056,13 +1104,13 @@
          (cons 'files paths)
          (cons 'added_lines_sample (bounded-list lines local-provider-input-line-limit)))))
 (def (local-embedding-provider-warning repo cmd)
-  (cond [(null? cmd)
-         "embedding provider requested but no local_embedding_provider_command config or JERBOA_AIGIT_LOCAL_EMBEDDING_PROVIDER is set"]
-        [(not (path-absolute? (car cmd)))
-         "embedding provider command must be an absolute path outside the scanned repository"]
-        [(path-inside-root? (car cmd) repo)
-         "embedding provider command inside the scanned repository was refused"]
-        [else #f]))
+  (provider-common-warning repo cmd
+                           "embedding provider requested but no local_embedding_provider_command config or JERBOA_AIGIT_LOCAL_EMBEDDING_PROVIDER is set"
+                           "embedding provider command must be an absolute path outside the scanned repository"
+                           "embedding provider command inside the scanned repository was refused"
+                           "embedding provider command must be canonical and not a symlink"
+                           "embedding provider command is group/world-writable and was refused"
+                           "embedding provider command path does not exist or cannot be canonicalized"))
 (def (local-embedding-provider-signal obj)
   (let ([score (provider-score obj)])
     (if score
diff --git a/tests/fixture-smoke.sh b/tests/fixture-smoke.sh
index 4635e60..24bb183 100755
--- a/tests/fixture-smoke.sh
+++ b/tests/fixture-smoke.sh
@@ -404,6 +404,38 @@ repo_embedding_json=$("$root/bin/jerboa-aigit" scan "$fixture" --config "$repo_e
 printf '%s\n' "$repo_embedding_json" | grep -q '"embeddings_used":false'
 printf '%s\n' "$repo_embedding_json" | grep -q 'embedding provider command inside the scanned repository was refused'
 
+safe_provider_target="$provider_tmp/safe-provider-target.sh"
+{
+  printf '#!/usr/bin/env sh\n'
+  printf 'printf '\''{"score":0.50,"reason":"symlink target"}\\n'\''\n'
+} > "$safe_provider_target"
+chmod 755 "$safe_provider_target"
+symlink_provider="$provider_tmp/symlink-provider.sh"
+ln -s "$safe_provider_target" "$symlink_provider"
+symlink_provider_config="$fixture/symlink-provider.json"
+printf '{"local_provider_command":["%s"]}\n' "$symlink_provider" > "$symlink_provider_config"
+symlink_provider_json=$("$root/bin/jerboa-aigit" scan "$fixture" --config "$symlink_provider_config" --format json --count 1 --llm --provider local)
+printf '%s\n' "$symlink_provider_json" | grep -q '"llm_used":false'
+printf '%s\n' "$symlink_provider_json" | grep -q 'local provider command must be canonical and not a symlink'
+
+world_writable_provider="$provider_tmp/world-writable-provider.sh"
+{
+  printf '#!/usr/bin/env sh\n'
+  printf 'printf '\''{"score":0.50,"reason":"unsafe mode"}\\n'\''\n'
+} > "$world_writable_provider"
+chmod 777 "$world_writable_provider"
+world_writable_provider_config="$fixture/world-writable-provider.json"
+printf '{"local_provider_command":["%s"]}\n' "$world_writable_provider" > "$world_writable_provider_config"
+world_writable_provider_json=$("$root/bin/jerboa-aigit" scan "$fixture" --config "$world_writable_provider_config" --format json --count 1 --llm --provider local)
+printf '%s\n' "$world_writable_provider_json" | grep -q '"llm_used":false'
+printf '%s\n' "$world_writable_provider_json" | grep -q 'local provider command is group/world-writable and was refused'
+
+world_writable_embedding_config="$fixture/world-writable-embedding-provider.json"
+printf '{"local_embedding_provider_command":["%s"]}\n' "$world_writable_provider" > "$world_writable_embedding_config"
+world_writable_embedding_json=$("$root/bin/jerboa-aigit" scan "$fixture" --config "$world_writable_embedding_config" --format json --count 1 --embeddings)
+printf '%s\n' "$world_writable_embedding_json" | grep -q '"embeddings_used":false'
+printf '%s\n' "$world_writable_embedding_json" | grep -q 'embedding provider command is group/world-writable and was refused'
+
 provider_script="$provider_tmp/local-provider.sh"
 {
   printf '#!/usr/bin/env sh\n'