Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
26 changes: 26 additions & 0 deletions THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Third-party notices

This file lists third-party material present in the Nav Center source repository. The Nav Center app bundle itself contains no third-party code (atsim is a review snapshot only).

## Nav Center

Nav Center is licensed under the MIT License. See [LICENSE](LICENSE).

## atsim (`vendor/atsim`)

- Upstream repository: https://github.com/austinkennethtucker/cli
- Snapshot commit: `cc37c5b1e3a4f7dfe17d9f043eb18021ff6faef4`
- Copied: 2026-09-15
- Package version: 0.1.0

This snapshot is not built, bundled, or executed by Nav Center.

The upstream pyproject.toml declares MIT licensing. No standalone LICENSE file was present in the package or upstream repository root at this commit.

License text: PENDING UPSTREAM CONFIRMATION

Distribution builds refuse to proceed while this marker remains.

## @opencode-ai/sdk

`@opencode-ai/sdk` is present in the snapshot's package manifest and lockfile. It is not installed and not distributed.
4 changes: 2 additions & 2 deletions docs/DEPENDENCIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ This inventory describes source dependencies, not a complete binary SBOM. Review
| Ruby, Pandoc, Chrome, Poppler | Optional/native runtime helpers, discovered on the user's machine | Not bundled by this tooling change; validate versions and licenses before bundling |
| Codex app-server | Optional independently installed/signed-in tool | Not bundled; no access to auth storage for tests |
| External atsim | Existing optional executable integration | Copied source does not activate it |
| `vendor/atsim` | Unchanged review snapshot at `cc37c5b1e3a4f7dfe17d9f043eb18021ff6faef4`; 11 upstream file hashes | Upstream declares MIT, standalone attribution/license notice missing. Resolve before distribution |
| `vendor/atsim` | Unchanged review snapshot at `cc37c5b1e3a4f7dfe17d9f043eb18021ff6faef4`; 11 upstream file hashes; not built, bundled, or shipped | See [THIRD_PARTY_NOTICES.md](../THIRD_PARTY_NOTICES.md). `PENDING UPSTREAM CONFIRMATION` is a distribution gate: `--distribution` refuses to build while that marker remains |
| Snapshot JS helper | `@opencode-ai/sdk` in the copied package/lock; not installed or integrated | Review lock and transitive licenses/security explicitly with a snapshot update |
| Development binaries | Gitleaks, actionlint, zizmor, SwiftLint from official pinned releases | Versions and archive SHA-256 in `scripts/tool-versions.json`; not shipped in the app |
| GitHub Actions | Full SHA references in `.github/workflows` | Weekly Dependabot PRs; human review of changes and permissions |
| SonarQube Cloud | Optional source/analysis service, maintained Swift only | OSS onboarding and account permissions must be verified; not a package dependency |

`python3 -B scripts/verify-vendor.py` checks both hashes and the exact snapshot file inventory. A matching manifest is integrity evidence, not upstream security or license approval. Never let dependency bots rewrite the snapshot implicitly.
`python3 -B scripts/verify-vendor.py` checks both hashes and the exact snapshot file inventory, and requires [THIRD_PARTY_NOTICES.md](../THIRD_PARTY_NOTICES.md) to name `vendor/atsim` and the snapshot commit from `vendor/atsim/UPSTREAM.md`. While that notice contains `PENDING UPSTREAM CONFIRMATION`, the command reports the open distribution gate and still exits successfully. A matching manifest is integrity evidence, not upstream security or license approval. Never let dependency bots rewrite the snapshot implicitly.

The prepared protected release workflow exports GitHub's SPDX dependency graph through [`scripts/export-sbom.py`](../scripts/export-sbom.py) and includes this file with the final candidate. The asynchronous GitHub API reports the repository graph at generation time, not an inventory guaranteed to match the release SHA. Its metadata records retrieval time, package count, scope and digest. GitHub may omit copied sources, system frameworks, and dynamically discovered tools; those limitations must remain visible. Retain the manifest, lockfiles, source revision, toolchain, final DMG checksum, signing/notarization evidence, and later provenance with an authorized release. [GitHub's current SBOM API](https://docs.github.com/en/rest/dependency-graph/sboms).
1 change: 1 addition & 0 deletions docs/PUBLIC_RELEASE_CHECKLIST.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ Excluded:
5. Confirm the bundle identifier and signing/notarization plan.
6. Add release screenshots only after checking them for private data.
7. Create the GitHub repository only after the extracted tree is clean.
8. Confirm the atsim notice in `THIRD_PARTY_NOTICES.md`: replace `PENDING UPSTREAM CONFIRMATION` with the upstream-confirmed license text before any distribution build.

## Release Artifact Contract

Expand Down
2 changes: 2 additions & 0 deletions docs/RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ NAV_CENTER_VERSION=0.1.0-beta.1 NAV_CENTER_BUILD=1 \

The name ends in `-unsigned.dmg`. This mode never invokes signing or notarization. Both packaging modes use `swift build -c release`; ordinary `scripts/build-and-run.sh build` defaults to debug. Set `NAV_CENTER_DIST_DIR` to an absolute temporary directory for isolated checks. Neither mode stops running app instances. Existing DMG, checksum, or notary-result outputs are refused rather than overwritten.

`scripts/build-and-run.sh` stages `LICENSE` and `THIRD_PARTY_NOTICES.md` into the app bundle at `Contents/Resources`. `scripts/package-beta-dmg.sh` copies both files to the DMG root beside the app and the Applications symlink. `--distribution` refuses to build while `THIRD_PARTY_NOTICES.md` contains `PENDING UPSTREAM CONFIRMATION`. `--local` still builds an unsigned image when that marker is present and prints the unsigned warning.

## Version and architecture

Set `NAV_CENTER_VERSION` to a numeric `major.minor.patch` with an optional prerelease suffix, and `NAV_CENTER_BUILD` to a positive integer. The app embeds the numeric version in `CFBundleShortVersionString`, the build number in `CFBundleVersion`, and the complete prerelease version in `NavCenterVersion`. Use a new build number for a new build.
Expand Down
11 changes: 11 additions & 0 deletions scripts/build-and-run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,15 @@ stage_icon() {
}

stage_app() {
if [[ ! -f "$ROOT_DIR/LICENSE" ]]; then
echo "missing LICENSE: $ROOT_DIR/LICENSE" >&2
exit 1
fi
if [[ ! -f "$ROOT_DIR/THIRD_PARTY_NOTICES.md" ]]; then
echo "missing THIRD_PARTY_NOTICES.md: $ROOT_DIR/THIRD_PARTY_NOTICES.md" >&2
exit 1
fi

swift build --package-path "$ROOT_DIR" -c "$CONFIGURATION" --product "$EXECUTABLE_NAME"
swift build --package-path "$ROOT_DIR" -c "$CONFIGURATION" --product "$CTL_NAME"
local build_binary
Expand All @@ -84,6 +93,8 @@ stage_app() {
cp "$ctl_build_binary" "$CTL_BINARY"
chmod +x "$APP_BINARY"
chmod +x "$CTL_BINARY"
cp "$ROOT_DIR/LICENSE" "$APP_RESOURCES/LICENSE"
cp "$ROOT_DIR/THIRD_PARTY_NOTICES.md" "$APP_RESOURCES/THIRD_PARTY_NOTICES.md"
stage_icon

local env_plist=""
Expand Down
7 changes: 7 additions & 0 deletions scripts/package-beta-dmg.sh
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,20 @@ for output in "$DMG_PATH" "$DMG_PATH.sha256" "$DMG_PATH.notary.json"; do
[[ ! -e "$output" && ! -L "$output" ]] || { echo "Refusing to overwrite release output: $output" >&2; exit 1; }
done

if [[ "$MODE" == --distribution ]] && grep -q "PENDING UPSTREAM CONFIRMATION" "$ROOT_DIR/THIRD_PARTY_NOTICES.md"; then
echo "Refusing distribution build: THIRD_PARTY_NOTICES.md contains PENDING UPSTREAM CONFIRMATION." >&2
exit 1
fi

NAV_CENTER_VERSION="$VERSION" NAV_CENTER_BUILD="$BUILD_NUMBER" NAV_CENTER_BUILD_CONFIGURATION=release \
NAV_CENTER_INCLUDE_WORKSPACE_ENV=0 "$ROOT_DIR/scripts/build-and-run.sh" build

STAGING_DIR="$(mktemp -d "$DIST_DIR/.dmg-stage.XXXXXX")"
trap 'rm -rf "$STAGING_DIR"' EXIT
cp -R "$APP_BUNDLE" "$STAGING_DIR/"
ln -s /Applications "$STAGING_DIR/Applications"
cp "$ROOT_DIR/LICENSE" "$STAGING_DIR/LICENSE"
cp "$ROOT_DIR/THIRD_PARTY_NOTICES.md" "$STAGING_DIR/THIRD_PARTY_NOTICES.md"
STAGED_APP="$STAGING_DIR/Nav Center.app"
for binary in NavCenterApp navcenterctl; do
[[ "$(lipo -archs "$STAGED_APP/Contents/MacOS/$binary")" == "$ARCH" ]] || { echo "Unexpected binary architecture: $binary" >&2; exit 1; }
Expand Down
126 changes: 125 additions & 1 deletion scripts/tests/test_release_scripts.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,12 @@
if name == "codesign": event += ":verify" if "--verify" in args else ":sign"
if name == "gitleaks": event += ":" + args[0]
if name == "git": event += ":status" if "status" in args else ":rev-parse"
payload = {"tool": name, "args": args, "event": event}
if name == "hdiutil" and args[:1] == ["create"] and "-srcfolder" in args:
source = pathlib.Path(args[args.index("-srcfolder") + 1])
payload["srcfolder"] = sorted(child.name for child in source.iterdir())
with open(os.environ["RELEASE_TRACE"], "a") as stream:
stream.write(json.dumps({"tool": name, "args": args, "event": event}) + "\n")
stream.write(json.dumps(payload) + "\n")
if os.environ.get("FAIL_EVENT") == event: sys.exit(9)
if name == "git" and "--is-shallow-repository" in args: print(os.environ.get("RELEASE_SHALLOW", "false"))
if name == "git" and "status" in args and os.environ.get("RELEASE_DIRTY"): print(" M synthetic.swift")
Expand Down Expand Up @@ -157,6 +161,8 @@ def setUp(self):
(self.root / "scripts").mkdir()
(self.root / "Resources").mkdir()
(self.root / "Resources/AppIcon.png").write_bytes(b"synthetic icon source")
(self.root / "LICENSE").write_text("Synthetic Nav Center license.\n")
(self.root / "THIRD_PARTY_NOTICES.md").write_text("Synthetic third-party notices.\n")
for script in (REPO / "scripts").glob("*.sh"):
shutil.copy2(script, self.root / "scripts" / script.name)
self.bin = self.root / "built"
Expand Down Expand Up @@ -409,6 +415,124 @@ def test_workflow_upload_follows_required_gates_and_uses_least_privilege(self):
self.assertIn("${{ runner.temp }}/nav-center-current-secrets.json", reports)
self.assertIn("${{ runner.temp }}/nav-center-history-secrets.json", reports)

def test_build_stages_license_and_notices_into_resources(self):
self.assert_ok(self.run_script("build-and-run.sh", "build"))
resources = self.dist / "Nav Center.app/Contents/Resources"
self.assertEqual((resources / "LICENSE").read_text(), (self.root / "LICENSE").read_text())
self.assertEqual((resources / "THIRD_PARTY_NOTICES.md").read_text(), (self.root / "THIRD_PARTY_NOTICES.md").read_text())

def test_build_fails_before_swift_when_license_or_notices_missing(self):
for name in ("LICENSE", "THIRD_PARTY_NOTICES.md"):
with self.subTest(name=name):
target = self.root / name
backup = target.read_text()
target.unlink()
result = self.run_script("build-and-run.sh", "build")
self.assertNotEqual(result.returncode, 0)
self.assertIn(name, result.stderr)
self.assertEqual(self.events(), [])
target.write_text(backup)
self.trace.unlink(missing_ok=True)

def test_local_package_places_license_and_notices_beside_app_in_image(self):
notices = self.root / "THIRD_PARTY_NOTICES.md"
notices.write_text(notices.read_text() + "License text: PENDING UPSTREAM CONFIRMATION\n")
self.assert_ok(self.run_script("package-beta-dmg.sh", "--local"))
created = [event for event in self.events() if event["tool"] == "hdiutil" and event["args"][0] == "create"]
self.assertEqual(len(created), 1)
for name in ("Nav Center.app", "Applications", "LICENSE", "THIRD_PARTY_NOTICES.md"):
self.assertIn(name, created[0]["srcfolder"])

def test_distribution_refuses_pending_notice_placeholder(self):
(self.root / "THIRD_PARTY_NOTICES.md").write_text("License text: PENDING UPSTREAM CONFIRMATION\n")
result = self.run_script("package-beta-dmg.sh", "--distribution", extra=self.credentials())
self.assertNotEqual(result.returncode, 0)
self.assertIn("PENDING UPSTREAM CONFIRMATION", result.stderr)
self.assertFalse(any(event["tool"] in ("swift", "codesign", "xcrun", "hdiutil") for event in self.events()))


class VendorNoticeTests(unittest.TestCase):
def setUp(self):
self.temporary = tempfile.TemporaryDirectory(prefix="nav-center-vendor-notice-tests-")
self.addCleanup(self.temporary.cleanup)
self.root = Path(self.temporary.name)

def write_snapshot(self, commit):
vendor = self.root / "vendor/atsim"
vendor.mkdir(parents=True)
payload = b"synthetic snapshot\n"
(vendor / "snapshot.txt").write_bytes(payload)
manifest = {"snapshot.txt": hashlib.sha256(payload).hexdigest()}
(vendor / "UPSTREAM-SHA256.json").write_text(json.dumps(manifest) + "\n")
(vendor / "UPSTREAM.md").write_text(
"- Repository: https://github.com/austinkennethtucker/cli\n"
"- Source directory: `atsim/`\n"
f"- Commit: `{commit}`\n"
"- Copied: 2026-09-15\n"
"- Package version: 0.1.0\n"
)
scripts = self.root / "scripts"
scripts.mkdir(exist_ok=True)
shutil.copy2(REPO / "scripts/verify-vendor.py", scripts / "verify-vendor.py")

def run_verify(self, root=None):
script_root = self.root if root is None else root
return subprocess.run(
[sys.executable, "-B", str(script_root / "scripts/verify-vendor.py")],
cwd=script_root, capture_output=True, text=True,
)

def test_notices_reference_snapshot_commit_and_path(self):
real = subprocess.run(
[sys.executable, "-B", str(REPO / "scripts/verify-vendor.py")],
cwd=REPO, capture_output=True, text=True,
)
self.assertEqual(real.returncode, 0, real.stderr + real.stdout)
upstream = (REPO / "vendor/atsim/UPSTREAM.md").read_text()
commit = re.search(r"(?m)^- Commit: `([0-9a-f]{40})`", upstream).group(1)
notices = (REPO / "THIRD_PARTY_NOTICES.md").read_text()
self.assertIn("vendor/atsim", notices)
self.assertIn(commit, notices)
self.assertNotIn("Copyright", notices.split("## @opencode-ai/sdk", 1)[0].split("## atsim", 1)[1])

synthetic_commit = "0123456789abcdef0123456789abcdef01234567"
self.write_snapshot(synthetic_commit)
notice_path = self.root / "THIRD_PARTY_NOTICES.md"
notice_path.write_text(f"vendor/atsim\n{synthetic_commit}\n")
accepted = self.run_verify()
self.assertEqual(accepted.returncode, 0, accepted.stderr + accepted.stdout)

notice_path.write_text(f"{synthetic_commit}\n")
missing_path = self.run_verify()
self.assertNotEqual(missing_path.returncode, 0)
self.assertIn("vendor/atsim", missing_path.stderr)

notice_path.write_text("vendor/atsim\n")
missing_commit = self.run_verify()
self.assertNotEqual(missing_commit.returncode, 0)
self.assertIn("upstream commit", missing_commit.stderr)

notice_path.unlink()
missing_file = self.run_verify()
self.assertNotEqual(missing_file.returncode, 0)
self.assertIn("THIRD_PARTY_NOTICES.md", missing_file.stderr)

def test_verify_vendor_reports_pending_confirmation_without_failing(self):
commit = "0123456789abcdef0123456789abcdef01234567"
self.write_snapshot(commit)
notices = self.root / "THIRD_PARTY_NOTICES.md"
pending_line = "atsim notice is pending upstream confirmation and remains a distribution gate."
notices.write_text(f"vendor/atsim\n{commit}\nLicense text: PENDING UPSTREAM CONFIRMATION\n")
pending = self.run_verify()
self.assertEqual(pending.returncode, 0, pending.stderr + pending.stdout)
self.assertIn(pending_line, pending.stdout)

notices.write_text(f"vendor/atsim\n{commit}\n")
confirmed = self.run_verify()
self.assertEqual(confirmed.returncode, 0, confirmed.stderr + confirmed.stdout)
self.assertNotIn(pending_line, confirmed.stdout)
self.assertNotIn("pending upstream confirmation", confirmed.stdout)


if __name__ == "__main__":
unittest.main()
19 changes: 18 additions & 1 deletion scripts/verify-vendor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
"""Check the reviewed ATS snapshot without installing or executing it."""
import hashlib
import json
import re
from pathlib import Path

root = Path(__file__).resolve().parents[1] / "vendor/atsim"
repo = Path(__file__).resolve().parents[1]
root = repo / "vendor/atsim"
manifest = json.loads((root / "UPSTREAM-SHA256.json").read_text())
for relative, expected in manifest.items():
path = root / relative
Expand All @@ -16,4 +18,19 @@
allowed = set(manifest) | {"UPSTREAM.md", "UPSTREAM-SHA256.json"}
if actual != allowed:
raise SystemExit(f"Snapshot file inventory changed: {sorted(actual ^ allowed)}")
notices_path = repo / "THIRD_PARTY_NOTICES.md"
if not notices_path.is_file():
raise SystemExit("Missing THIRD_PARTY_NOTICES.md")
notices = notices_path.read_text()
upstream = (root / "UPSTREAM.md").read_text()
commit_match = re.search(r"(?m)^- Commit: `([0-9a-f]{40})`", upstream)
if commit_match is None:
raise SystemExit("UPSTREAM.md does not record an upstream commit SHA")
commit = commit_match.group(1)
if "vendor/atsim" not in notices:
raise SystemExit("THIRD_PARTY_NOTICES.md does not mention vendor/atsim")
if commit not in notices:
raise SystemExit("THIRD_PARTY_NOTICES.md does not contain the upstream commit recorded in UPSTREAM.md")
print(f"Verified all {len(manifest)} upstream files; snapshot attribution remains a distribution gate.")
if "PENDING UPSTREAM CONFIRMATION" in notices:
print("atsim notice is pending upstream confirmation and remains a distribution gate.")
Loading