Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
16f20e1
Add shadow metadata to one-shot reconciliation reports
hnic Jun 25, 2026
0f71d25
Add runtime topology readiness checks
hnic Jun 25, 2026
d007f2d
Scrub secrets in redaction boundaries and workflow storage
hnic Jun 25, 2026
cbb193e
Add operator inspection to structured execution plan
hnic Jun 25, 2026
abf1a95
Add production governance validation
hnic Jun 25, 2026
2fbc153
Add typed-tool exception admission checks
hnic Jun 25, 2026
019c3e3
Add production claim admission checks
hnic Jun 25, 2026
f11a867
feat(coding-pr-delivery): add evidence runbook projection
hnic Jun 25, 2026
fac1ae8
feat(coding-pr-delivery): add production profile facade
hnic Jun 25, 2026
6505ae0
Add evidence packet validation facade
hnic Jun 25, 2026
370d03d
Add review packet validation to production profile
hnic Jun 25, 2026
d36b7bf
Add review decision facade delegation
hnic Jun 25, 2026
c31d2bb
Add enablement request validation to production profile
hnic Jun 25, 2026
aee3b08
Add operator apply plan delegation
hnic Jun 25, 2026
8cec68c
Add operator apply record validation facade
hnic Jun 25, 2026
27fad4c
Add phase 2 claim template delegates
hnic Jun 25, 2026
74aeeb4
Add production profile packet templates
hnic Jun 25, 2026
1694072
Add production profile template delegates
hnic Jun 25, 2026
539560a
Add observation status validation delegate
hnic Jun 25, 2026
b5ee35f
Add observation status delegates to production profile
hnic Jun 25, 2026
7db8ed4
test(coding-pr-delivery): cover TAPD CNB shadow chain
hnic Jun 25, 2026
7ac19b9
Add linear CNB shadow phase2 claim template
hnic Jun 25, 2026
513a846
Generalize production profile shadow chain tests
hnic Jun 25, 2026
79c8905
Add shadow run evidence packet coverage
hnic Jun 25, 2026
36dd594
Extend production packet test coverage for shadow claims
hnic Jun 25, 2026
5aa4751
Add Linear shadow review template test
hnic Jun 25, 2026
9942602
Add Linear CNB shadow review packet test
hnic Jun 25, 2026
3946fed
Expand production review decision test coverage
hnic Jun 25, 2026
15ca89b
Add linear CNB shadow enablement request test
hnic Jun 25, 2026
09c5926
Allow Linear shadow enablement requests
hnic Jun 25, 2026
328e393
Add Linear CNB shadow apply plan test coverage
hnic Jun 25, 2026
b08d2a9
Add Linear CNB shadow coverage to production profile tests
hnic Jun 25, 2026
5c517a5
Add Linear+CNB shadow claim tests
hnic Jun 25, 2026
46ab038
Add Linear CNB shadow coverage tests
hnic Jun 25, 2026
b9a8e9b
Add typed tool exception coverage for Linear CNB shadow raw access
hnic Jun 25, 2026
11e717d
Preserve shadow provider identity in reconciliation tests
hnic Jun 25, 2026
2280797
Add shadow identity coverage to dry-run JSON output
hnic Jun 25, 2026
558d9a1
Add reconcile coverage for confirmed CNB output
hnic Jun 25, 2026
a090e92
Add one-shot report coverage for Linear and CNB identity
hnic Jun 25, 2026
9574abf
Test Linear CNB target payload sanitization
hnic Jun 25, 2026
dfaac7e
Preserve Linear CNB identity in state store tests
hnic Jun 25, 2026
60d493c
feat(coding-pr-delivery): add phase 2 evidence plans
hnic Jun 25, 2026
2e27682
feat(coding-pr-delivery): add phase 4 review plans
hnic Jun 25, 2026
0b4a3ff
test(structured-plan): avoid secret scan fixture literal
hnic Jun 25, 2026
56b4bce
test(structured-plan): avoid secret scan fixture literal
hnic Jun 25, 2026
112c9f4
feat(coding-pr-delivery): export production profile plans
hnic Jun 25, 2026
0cb37c9
feat(coding-pr-delivery): validate production profile packets
hnic Jun 25, 2026
74251f5
feat(coding-pr-delivery): generate production profile templates
hnic Jun 25, 2026
e20977a
fix(coding-pr-delivery): cite owning review specs
hnic Jun 25, 2026
1f58a2d
feat(coding-pr-delivery): require scrubber rule evidence
hnic Jun 25, 2026
534abf0
feat(coding-pr-delivery): expose phase2 preflight checks
hnic Jun 25, 2026
b1695dc
feat(coding-pr-delivery): validate phase2 preflight blockers
hnic Jun 25, 2026
8ad97ab
feat(coding-pr-delivery): require review preflight reports
hnic Jun 25, 2026
58c7d44
feat(coding-pr-delivery): harden evidence packet refs
hnic Jun 25, 2026
bb035d3
test(coding-pr-delivery): avoid secret scan fixture literal
hnic Jun 25, 2026
b0ae2c6
feat(coding-pr-delivery): template preflight reports
hnic Jun 25, 2026
424ee7d
feat(coding-pr-delivery): require preflight evidence refs
hnic Jun 25, 2026
5c42fd6
feat(coding-pr-delivery): harden observation evidence refs
hnic Jun 25, 2026
1b699d3
feat(coding-pr-delivery): harden claim evidence refs
hnic Jun 25, 2026
24736e4
feat(coding-pr-delivery): collect preflight reports
hnic Jun 25, 2026
68ae1b8
feat(coding-pr-delivery): project production status
hnic Jun 25, 2026
475d6b4
feat(coding-pr-delivery): request provider evidence
hnic Jun 25, 2026
1e03c70
feat(coding-pr-delivery): bundle provider evidence
hnic Jun 25, 2026
c897c11
feat(coding-pr-delivery): package provider handoff
hnic Jun 25, 2026
2a41bd1
Use priv assets for migrations and exqlite
hnic Jun 29, 2026
ad91d6d
Bump container security workflow actions
hnic Jun 29, 2026
c2059d1
Retry container builds and simplify escript path handling
hnic Jun 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 39 additions & 19 deletions .github/workflows/container-security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,23 +73,43 @@ jobs:
run: bash -n scripts/container-security-scan.sh

- name: Build ${{ matrix.target }} image
id: build-image
run: |
set -euo pipefail
mkdir -p "$REPORT_DIR"
DOCKER_BUILDKIT=1 docker build \
-f docker/app/Dockerfile \
--target "${{ matrix.target }}" \
--build-arg "OPENCODE_VERSION=${OPENCODE_VERSION}" \
--build-arg "CODEX_VERSION=${CODEX_VERSION}" \
--build-arg "CLAUDE_CODE_VERSION=${CLAUDE_CODE_VERSION}" \
--build-arg "CODEBUDDY_VERSION=${CODEBUDDY_VERSION}" \
--build-arg "SYMPHONY_UID=${SYMPHONY_UID}" \
--build-arg "SYMPHONY_GID=${SYMPHONY_GID}" \
-t "$IMAGE_NAME" \
.

build_succeeded=false
for attempt in 1 2 3; do
echo "Docker build attempt ${attempt}/3"
if DOCKER_BUILDKIT=1 docker build \
-f docker/app/Dockerfile \
--target "${{ matrix.target }}" \
--build-arg "OPENCODE_VERSION=${OPENCODE_VERSION}" \
--build-arg "CODEX_VERSION=${CODEX_VERSION}" \
--build-arg "CLAUDE_CODE_VERSION=${CLAUDE_CODE_VERSION}" \
--build-arg "CODEBUDDY_VERSION=${CODEBUDDY_VERSION}" \
--build-arg "SYMPHONY_UID=${SYMPHONY_UID}" \
--build-arg "SYMPHONY_GID=${SYMPHONY_GID}" \
-t "$IMAGE_NAME" \
.; then
build_succeeded=true
break
fi

if [ "$attempt" -lt 3 ]; then
sleep $((attempt * 15))
fi
done

if [ "$build_succeeded" != "true" ]; then
exit 1
fi

echo "built=true" >> "$GITHUB_OUTPUT"

- name: Trivy vulnerability gate
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0
if: steps.build-image.outputs.built == 'true'
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
image-ref: ${{ env.IMAGE_NAME }}
format: table
Expand All @@ -98,8 +118,8 @@ jobs:
exit-code: "1"

- name: Trivy JSON report
if: always()
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0
if: steps.build-image.outputs.built == 'true'
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
image-ref: ${{ env.IMAGE_NAME }}
format: json
Expand All @@ -109,17 +129,17 @@ jobs:
exit-code: "0"

- name: Generate SPDX SBOM
if: always()
uses: anchore/sbom-action@f8bdd1d8ac5e901a77a92f111440fdb1b593736b # v0.20.6
if: steps.build-image.outputs.built == 'true'
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
with:
image: ${{ env.IMAGE_NAME }}
format: spdx-json
output-file: ${{ env.REPORT_DIR }}/sbom.spdx.json
dependency-snapshot: false

- name: Generate CycloneDX SBOM
if: always()
uses: anchore/sbom-action@f8bdd1d8ac5e901a77a92f111440fdb1b593736b # v0.20.6
if: steps.build-image.outputs.built == 'true'
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
with:
image: ${{ env.IMAGE_NAME }}
format: cyclonedx-json
Expand All @@ -128,7 +148,7 @@ jobs:

- name: Upload container security reports
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: container-security-${{ matrix.target }}
path: ${{ env.REPORT_DIR }}
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,6 @@ Thumbs.db
# Local runtime logs and specs (Existing)
# ==========================================
/log/
specs/
specs/

maestro-book
200 changes: 200 additions & 0 deletions elixir/lib/symphony_elixir/platform/priv_assets.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
defmodule SymphonyElixir.Platform.PrivAssets do
@moduledoc false

@default_otp_app :symphony_elixir
@cache_rootname "symphony-elixir-priv-assets"

@spec app_priv_root!(Path.t(), keyword()) :: Path.t()
def app_priv_root!(relative_dir, opts \\ []) when is_binary(relative_dir) and is_list(opts) do
otp_app = Keyword.get(opts, :otp_app, @default_otp_app)
relative_dir = validate_relative_dir!(relative_dir)

otp_app
|> priv_dir!()
|> real_priv_dir!()
|> Path.join(relative_dir)
|> Path.expand()
end

defp priv_dir!(otp_app) when is_atom(otp_app) and not is_nil(otp_app) do
case :code.priv_dir(otp_app) do
path when is_list(path) ->
to_string(path)

{:error, reason} ->
raise ArgumentError,
"could not resolve priv directory for otp_app #{inspect(otp_app)}: #{inspect(reason)}"
end
end

defp priv_dir!(otp_app) do
raise ArgumentError, "application priv asset otp_app must be an atom, got #{inspect(otp_app)}"
end

defp real_priv_dir!(priv_dir) when is_binary(priv_dir) do
if File.dir?(priv_dir) do
priv_dir
else
extracted_priv_dir!(priv_dir)
end
end

defp extracted_priv_dir!(expected_priv_dir) do
script_path = escript_path!()
cache_root = extraction_cache_root!(script_path)
priv_dir = ensure_extracted_priv_dir!(script_path, cache_root, expected_priv_dir)
add_extracted_code_paths(cache_root)

if File.dir?(priv_dir) do
priv_dir
else
raise ArgumentError,
"could not resolve extracted application priv directory: #{inspect(priv_dir)}"
end
end

defp add_extracted_code_paths(cache_root) do
[cache_root, "*", "ebin"]
|> Path.join()
|> Path.wildcard()
|> Enum.sort()
|> Enum.each(&:code.add_patha(String.to_charlist(&1)))
end

defp escript_path! do
script_path =
:escript.script_name()
|> List.to_string()
|> Path.expand()

cond do
script_path in ["", Path.expand("-e")] ->
raise ArgumentError, "could not resolve escript path for application priv assets"

File.regular?(script_path) ->
script_path

true ->
raise ArgumentError,
"could not resolve escript path for application priv assets: #{inspect(script_path)}"
end
end

defp extraction_cache_root!(script_path) do
case File.stat(script_path) do
{:ok, %File.Stat{size: size, mtime: mtime}} ->
cache_key = :erlang.phash2({script_path, size, mtime}) |> Integer.to_string(36)
Path.join([System.tmp_dir!(), @cache_rootname, cache_key])

{:error, reason} ->
raise ArgumentError,
"could not stat escript for application priv assets: #{inspect(script_path)} reason=#{inspect(reason)}"
end
end

defp ensure_extracted_priv_dir!(script_path, cache_root, expected_priv_dir) do
expected_priv_dir = Path.expand(expected_priv_dir)

case find_extracted_priv_dir(cache_root, script_path, expected_priv_dir) do
{:ok, priv_dir} ->
priv_dir

:error ->
File.rm_rf!(cache_root)
File.mkdir_p!(cache_root)

script_path
|> escript_archive!()
|> unzip_archive!(cache_root)

case find_extracted_priv_dir(cache_root, script_path, expected_priv_dir) do
{:ok, priv_dir} ->
priv_dir

:error ->
raise ArgumentError,
"could not find bundled application priv directory after extracting #{inspect(script_path)}"
end
end
end

defp find_extracted_priv_dir(cache_root, script_path, expected_priv_dir) do
direct_candidate =
expected_priv_dir
|> Path.relative_to(script_path)
|> direct_candidate(cache_root)

cond do
is_binary(direct_candidate) and File.dir?(direct_candidate) ->
{:ok, direct_candidate}

true ->
[cache_root, "**", "priv"]
|> Path.join()
|> Path.wildcard()
|> Enum.find(&File.dir?/1)
|> case do
nil -> :error
priv_dir -> {:ok, priv_dir}
end
end
end

defp direct_candidate(relative_path, cache_root) do
if Path.type(relative_path) == :relative do
Path.join(cache_root, relative_path)
end
end

defp escript_archive!(script_path) do
case :escript.extract(String.to_charlist(script_path), []) do
{:ok, sections} ->
case Keyword.fetch(sections, :archive) do
{:ok, archive_bin} when is_binary(archive_bin) ->
archive_bin

{:ok, _section} ->
raise ArgumentError, "invalid escript archive for application priv assets"

:error ->
raise ArgumentError, "missing escript archive for application priv assets"
end

{:error, reason} ->
raise ArgumentError, "could not extract escript archive for application priv assets: #{inspect(reason)}"
end
end

defp unzip_archive!(archive_bin, cache_root) do
case :zip.extract(archive_bin, [{:cwd, String.to_charlist(cache_root)}]) do
{:ok, _paths} ->
cache_root

{:error, reason} ->
raise ArgumentError, "could not unzip escript archive for application priv assets: #{inspect(reason)}"
end
end

defp validate_relative_dir!(relative_dir) do
relative_dir = String.trim(relative_dir)
segments = Path.split(relative_dir)

cond do
relative_dir == "" ->
raise ArgumentError, "application priv asset root must be non-empty"

Path.type(relative_dir) == :absolute ->
raise ArgumentError, "application priv asset root must be relative"

contains_forbidden_relative_segment?(segments) ->
raise ArgumentError, "application priv asset root must stay under the application priv directory"

true ->
Path.join(segments)
end
end

defp contains_forbidden_relative_segment?(segments) do
Enum.any?(segments, &(&1 in [".", ".."]))
end
end
35 changes: 34 additions & 1 deletion elixir/lib/symphony_elixir/storage/migrator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ defmodule SymphonyElixir.Storage.Migrator do
for the local service profile.
"""

alias SymphonyElixir.Platform.PrivAssets
alias SymphonyElixir.Storage.{ErrorCodes, Repo}

@spec child_spec(keyword()) :: Supervisor.child_spec()
Expand All @@ -31,7 +32,7 @@ defmodule SymphonyElixir.Storage.Migrator do

@spec migrate(module()) :: :ok | {:error, map()}
def migrate(repo \\ Repo) do
path = Ecto.Migrator.migrations_path(repo)
path = migrations_path(repo)
compiler_options = Code.compiler_options()

try do
Expand All @@ -54,4 +55,36 @@ defmodule SymphonyElixir.Storage.Migrator do
reason: reason
}
end

defp migrations_path(repo) do
repo
|> repo_priv_root()
|> Path.join("migrations")
|> PrivAssets.app_priv_root!(otp_app: repo_otp_app(repo))
end

defp repo_priv_root(repo) do
repo
|> repo_config()
|> Keyword.get(:priv, "priv/storage_repo")
|> strip_priv_prefix()
end

defp repo_otp_app(repo) do
repo
|> repo_config()
|> Keyword.get(:otp_app, :symphony_elixir)
end

defp repo_config(repo) do
if function_exported?(repo, :config, 0) do
repo.config()
else
[]
end
end

defp strip_priv_prefix("priv/" <> rest), do: rest
defp strip_priv_prefix("priv"), do: ""
defp strip_priv_prefix(priv) when is_binary(priv), do: priv
end
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ defmodule SymphonyElixir.Storage.Redaction.DefaultBackend do

@behaviour SymphonyElixir.Storage.Redaction

alias SymphonyElixir.Observability.Redaction, as: ObservabilityRedaction

@redacted "[REDACTED]"
@sensitive_key_patterns [
~r/password/i,
Expand Down Expand Up @@ -49,6 +51,7 @@ defmodule SymphonyElixir.Storage.Redaction.DefaultBackend do
end

defp redact_value(tuple) when is_tuple(tuple), do: tuple |> Tuple.to_list() |> redact_value() |> List.to_tuple()
defp redact_value(value) when is_binary(value), do: ObservabilityRedaction.redact_string(value)
defp redact_value(value), do: value

defp sensitive_key?(key) when is_atom(key), do: key |> Atom.to_string() |> sensitive_key?()
Expand Down
Loading