diff --git a/elixir/README.md b/elixir/README.md index ab082541d7..8cf8d15171 100644 --- a/elixir/README.md +++ b/elixir/README.md @@ -195,10 +195,10 @@ Notes: - For `tracker.kind: github`, set `tracker.project_slug` to `owner/repo`. - The GitHub tracker implementation uses GitHub Issues plus workflow labels for Symphony states; GitHub Projects v2 is not currently used as the tracker surface. - Safer Codex defaults are used when policy fields are omitted: - - `codex.approval_policy` defaults to `{"reject":{"sandbox_approval":true,"rules":true,"mcp_elicitations":true}}` + - `codex.approval_policy` defaults to `never` - `codex.thread_sandbox` defaults to `workspace-write` - `codex.turn_sandbox_policy` defaults to a `workspaceWrite` policy rooted at the current issue workspace -- Supported `codex.approval_policy` values depend on the targeted Codex app-server version. In the current local Codex schema, string values include `untrusted`, `on-failure`, `on-request`, and `never`, and object-form `reject` is also supported. +- Supported `codex.approval_policy` values depend on the targeted Codex app-server version. In the current local Codex schema, string values include `untrusted`, `on-failure`, `on-request`, `never`, and `granular`. - Supported `codex.thread_sandbox` values: `read-only`, `workspace-write`, `danger-full-access`. - When `codex.turn_sandbox_policy` is set explicitly, Symphony passes the map through to Codex unchanged. Compatibility then depends on the targeted Codex app-server version rather than local @@ -212,7 +212,7 @@ Notes: - If a hook needs `mise exec` inside a freshly cloned workspace, trust the repo config and fetch the project dependencies in `hooks.after_create` before invoking `mise` later from other hooks. - `tracker.api_key` reads from `LINEAR_API_KEY` when unset or when value is `$LINEAR_API_KEY`. -- `tracker.api_key` also reads from `GITLAB_API_TOKEN` for `gitlab` and `GITHUB_TOKEN` for `github` when unset or expressed as the matching `$ENV_VAR`. +- `tracker.api_key` also reads from `GITLAB_API_TOKEN` for `gitlab` and `GITHUB_TOKEN` (or `GH_TOKEN`) for `github` when unset or expressed as the matching `$ENV_VAR`. - For path values, `~` is expanded to the home directory. - For env-backed path values, use `$VAR`. `workspace.root` resolves `$VAR` before path handling, while `codex.command` stays a shell command string and any `$VAR` expansion there happens in the diff --git a/elixir/lib/symphony_elixir/config/schema.ex b/elixir/lib/symphony_elixir/config/schema.ex index 53fd41ed81..b1efb97773 100644 --- a/elixir/lib/symphony_elixir/config/schema.ex +++ b/elixir/lib/symphony_elixir/config/schema.ex @@ -160,15 +160,7 @@ defmodule SymphonyElixir.Config.Schema do embedded_schema do field(:command, :string, default: "codex app-server") - field(:approval_policy, StringOrMap, - default: %{ - "reject" => %{ - "sandbox_approval" => true, - "rules" => true, - "mcp_elicitations" => true - } - } - ) + field(:approval_policy, StringOrMap, default: "never") field(:thread_sandbox, :string, default: "workspace-write") field(:turn_sandbox_policy, :map) @@ -425,7 +417,7 @@ defmodule SymphonyElixir.Config.Schema do defp tracker_env_value(provider, callback) when is_atom(provider) and not is_nil(provider) and is_atom(callback) do case apply(provider, callback, []) do - env_name when is_binary(env_name) -> System.get_env(env_name) + env_name when is_binary(env_name) -> resolve_env_var(env_name) _ -> nil end end @@ -484,7 +476,7 @@ defmodule SymphonyElixir.Config.Schema do defp resolve_env_value(value, fallback) when is_binary(value) do case env_reference_name(value) do {:ok, env_name} -> - case System.get_env(env_name) do + case resolve_env_var(env_name) do nil -> fallback "" -> nil env_value -> env_value @@ -513,12 +505,18 @@ defmodule SymphonyElixir.Config.Schema do defp env_reference_name(_value), do: :error defp resolve_env_token(env_name) do - case System.get_env(env_name) do + case resolve_env_var(env_name) do nil -> :missing env_value -> env_value end end + defp resolve_env_var("GITHUB_TOKEN") do + System.get_env("GITHUB_TOKEN") || System.get_env("GH_TOKEN") + end + + defp resolve_env_var(env_name), do: System.get_env(env_name) + defp normalize_secret_value(value) when is_binary(value) do if value == "", do: nil, else: value end diff --git a/elixir/test/symphony_elixir/core_test.exs b/elixir/test/symphony_elixir/core_test.exs index 50e59ed07b..b4b04a4fd6 100644 --- a/elixir/test/symphony_elixir/core_test.exs +++ b/elixir/test/symphony_elixir/core_test.exs @@ -226,6 +226,53 @@ defmodule SymphonyElixir.CoreTest do assert :ok = Config.validate!() end + test "github api token falls back to GH_TOKEN when GITHUB_TOKEN is missing" do + previous_github_api_key = System.get_env("GITHUB_TOKEN") + previous_gh_token = System.get_env("GH_TOKEN") + env_api_key = "test-gh-token" + + on_exit(fn -> + restore_env("GITHUB_TOKEN", previous_github_api_key) + restore_env("GH_TOKEN", previous_gh_token) + end) + + System.delete_env("GITHUB_TOKEN") + System.put_env("GH_TOKEN", env_api_key) + + write_workflow_file!(Workflow.workflow_file_path(), + tracker_kind: "github", + tracker_api_token: nil, + tracker_endpoint: nil, + tracker_project_slug: "owner/repo", + codex_command: "/bin/sh app-server" + ) + + assert Config.settings!().tracker.api_key == env_api_key + end + + test "github api token prefers GITHUB_TOKEN over GH_TOKEN when both are set" do + previous_github_api_key = System.get_env("GITHUB_TOKEN") + previous_gh_token = System.get_env("GH_TOKEN") + + on_exit(fn -> + restore_env("GITHUB_TOKEN", previous_github_api_key) + restore_env("GH_TOKEN", previous_gh_token) + end) + + System.put_env("GITHUB_TOKEN", "preferred-github-token") + System.put_env("GH_TOKEN", "fallback-gh-token") + + write_workflow_file!(Workflow.workflow_file_path(), + tracker_kind: "github", + tracker_api_token: nil, + tracker_endpoint: nil, + tracker_project_slug: "owner/repo", + codex_command: "/bin/sh app-server" + ) + + assert Config.settings!().tracker.api_key == "preferred-github-token" + end + test "workflow file path defaults to WORKFLOW.md in the current working directory when app env is unset" do original_workflow_path = Workflow.workflow_file_path() @@ -1599,7 +1646,8 @@ defmodule SymphonyElixir.CoreTest do write_workflow_file!(Workflow.workflow_file_path(), workspace_root: workspace_root, - codex_command: "#{codex_binary} app-server" + codex_command: "#{codex_binary} app-server", + codex_approval_policy: nil ) issue = %Issue{ @@ -1630,13 +1678,7 @@ defmodule SymphonyElixir.CoreTest do |> String.trim_leading("JSON:") |> Jason.decode!() |> then(fn payload -> - expected_approval_policy = %{ - "reject" => %{ - "sandbox_approval" => true, - "rules" => true, - "mcp_elicitations" => true - } - } + expected_approval_policy = "never" payload["method"] == "thread/start" && get_in(payload, ["params", "approvalPolicy"]) == expected_approval_policy && @@ -1663,13 +1705,7 @@ defmodule SymphonyElixir.CoreTest do |> String.trim_leading("JSON:") |> Jason.decode!() |> then(fn payload -> - expected_approval_policy = %{ - "reject" => %{ - "sandbox_approval" => true, - "rules" => true, - "mcp_elicitations" => true - } - } + expected_approval_policy = "never" payload["method"] == "turn/start" && get_in(payload, ["params", "cwd"]) == canonical_workspace && diff --git a/elixir/test/symphony_elixir/workspace_and_config_test.exs b/elixir/test/symphony_elixir/workspace_and_config_test.exs index 295a1af895..c8cc85e7d4 100644 --- a/elixir/test/symphony_elixir/workspace_and_config_test.exs +++ b/elixir/test/symphony_elixir/workspace_and_config_test.exs @@ -746,13 +746,7 @@ defmodule SymphonyElixir.WorkspaceAndConfigTest do assert config.agent.max_concurrent_agents == 10 assert config.codex.command == "codex app-server" - assert config.codex.approval_policy == %{ - "reject" => %{ - "sandbox_approval" => true, - "rules" => true, - "mcp_elicitations" => true - } - } + assert config.codex.approval_policy == "never" assert config.codex.thread_sandbox == "workspace-write"