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
21 changes: 21 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,27 @@ jobs:
- name: Run tests
run: python3 -m pytest -v

# Event 179 — the app shell gets a build gate (E176 shipped it CI-blind;
# release-relevance ends that). Build only — launching a GUI on a runner
# proves nothing the compile doesn't.
app-build:
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Cargo cache
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
app/target
key: cargo-${{ runner.os }}-${{ hashFiles('app/Cargo.lock') }}

- name: Build app shell
run: cd app && cargo build --release

# Event 177 — the distribution acceptance bar as CI: a clean macOS runner
# with nothing but Python must go wheel → install → doctor clean → sync →
# resolvable hooks → a hook actually firing, with ZERO repo checkout in
Expand Down
62 changes: 62 additions & 0 deletions .github/workflows/release-assets.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Release assets (E179): build the wheel and the .pkg and attach both to a
# GitHub release.
#
# TRIGGER REALITY (the E163 deadlock class, documented not hidden):
# release-please cuts releases with GITHUB_TOKEN, and GitHub deliberately
# does not chain workflows off bot-token events — so `release: published`
# only fires once the operator gives release-please a PAT. Until then,
# `workflow_dispatch` is the working path: pick the tag, run, assets land.
#
# Signing: runners have no Developer ID cert; artifacts built here are the
# UNSIGNED tier. Once the operator enrolls (Apple Developer, operator-only
# gate), signing moves into scripts/build_app_pkg.sh via its env vars and a
# certificate-import step here.

name: release-assets

on:
workflow_dispatch:
inputs:
tag:
description: "Release tag to attach assets to (e.g. episteme-v1.10.0)"
required: true
release:
types: [published]

permissions:
contents: write

jobs:
build-and-attach:
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref }}

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Build wheel
run: python3 -m pip wheel . --no-deps -w dist_wheel

- name: Privacy gate (same contract as CI clean-install)
run: |
LISTING="$(python3 -m zipfile -l dist_wheel/*.whl)"
echo "$LISTING" | grep -q "_assets/core/hooks/workflow_guard.py" || { echo "assets missing"; exit 1; }
if echo "$LISTING" | grep -E "memory/global/(runtime_digest|operator_profile|agent_feedback|workflow_policy|cognitive_profile|overview|python_runtime_policy)\.md|skills/private|memory/(evolution|substrates)|\.jsonl|\.pem|\.key|\.p12"; then
echo "PRIVACY VIOLATION in release wheel"; exit 1; fi

- name: Build app + pkg
run: scripts/build_app_pkg.sh

- name: Attach assets
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.event.release.tag_name }}
run: |
gh release upload "$TAG" dist_wheel/*.whl dist/Episteme.pkg --clobber
echo "attached: $(ls dist_wheel/*.whl) dist/Episteme.pkg → $TAG"
26 changes: 22 additions & 4 deletions app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,25 @@ scripts/build_app_pkg.sh
Requires Rust ≥ 1.85 (`edition = "2024"` in `app/Cargo.toml`); `app/` is not
built in CI — the build host's toolchain is the only gate.

Produces `dist/Episteme.app` and an **unsigned** `dist/Episteme.pkg`
(installs to /Applications). Unsigned is a conscious tier: it installs
cleanly on the machine that built it; distribution-grade signing +
notarization is a separate operator-gated decision recorded in the ledger.
Produces `dist/Episteme.app` and `dist/Episteme.pkg` (installs to
/Applications), with the icon regenerated on demand:
`swift app/icon/make_icns.swift app/icon` (the committed `episteme.icns` is
a reproducible artifact of that script — no design blob without a source).

**Signing tiers** (E179 — the script is credential-ready and inert without
certs; each tier upgrades by exporting env vars before the build):

| Tier | Requires | Gets you |
|---|---|---|
| unsigned (default) | nothing | installs on the building machine; right-click → Open elsewhere |
| signed | `EPISTEME_SIGN_APP_ID`, `EPISTEME_SIGN_INSTALLER_ID` (Developer ID certs — operator-only Apple enrollment) | Gatekeeper-recognized identity |
| notarized | + `EPISTEME_NOTARY_PROFILE` (notarytool keychain profile) | clean first-run for any downloader |

Local builds carry `com.apple.provenance` AppleDouble entries in the pkg
payload — SIP-protected, re-stamped on write, unremovable by construction;
harmless 11-byte metadata (release artifacts are CI-built via
`release-assets.yml`). First check after enrolling: hardened runtime
(`--options runtime`) vs the wry webview — if the window comes up blank, add a JIT entitlement (noted in
`scripts/build_app_pkg.sh`). Release assets: `.github/workflows/release-assets.yml`
attaches the wheel + pkg to a GitHub release (`workflow_dispatch` today;
`release: published` activates once release-please gets the operator's PAT).
Binary file added app/icon/episteme.icns
Binary file not shown.
Binary file added app/icon/episteme.iconset/icon_128x128.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/icon/episteme.iconset/icon_128x128@2x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/icon/episteme.iconset/icon_16x16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/icon/episteme.iconset/icon_16x16@2x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/icon/episteme.iconset/icon_256x256.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/icon/episteme.iconset/icon_256x256@2x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/icon/episteme.iconset/icon_32x32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/icon/episteme.iconset/icon_32x32@2x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/icon/episteme.iconset/icon_512x512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/icon/episteme.iconset/icon_512x512@2x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
112 changes: 112 additions & 0 deletions app/icon/make_icns.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// Episteme.app icon generator (E179).
//
// The icns is a REPRODUCIBLE artifact: this script draws the mark with
// CoreGraphics (no design binary without a source), writes the full iconset,
// and runs iconutil. Regenerate with:
//
// swift app/icon/make_icns.swift app/icon
//
// Mark: the Gate — two accent brackets facing each other with the operator
// (a filled dot) between them, on the console's dark panel color. Geometry
// is proportional to size, with stroke widths tuned so the gate survives
// 16 px.

import AppKit
import CoreGraphics
import Foundation

let bg = CGColor(red: 0x0B / 255.0, green: 0x0E / 255.0, blue: 0x14 / 255.0, alpha: 1)
let edge = CGColor(red: 0x1E / 255.0, green: 0x25 / 255.0, blue: 0x30 / 255.0, alpha: 1)
let accent = CGColor(red: 0x5E / 255.0, green: 0xC2 / 255.0, blue: 0xA6 / 255.0, alpha: 1)

func drawIcon(size: Int) -> CGImage {
let s = CGFloat(size)
let ctx = CGContext(
data: nil, width: size, height: size, bitsPerComponent: 8, bytesPerRow: 0,
space: CGColorSpace(name: CGColorSpace.sRGB)!,
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
)!

// Background: rounded square (macOS icon-grid inset), subtle edge.
let inset = s * 0.06
let rect = CGRect(x: inset, y: inset, width: s - 2 * inset, height: s - 2 * inset)
let radius = rect.width * 0.225
let path = CGPath(roundedRect: rect, cornerWidth: radius, cornerHeight: radius, transform: nil)
ctx.addPath(path)
ctx.setFillColor(bg)
ctx.fillPath()
if size >= 64 {
ctx.addPath(path)
ctx.setStrokeColor(edge)
ctx.setLineWidth(max(1, s * 0.008))
ctx.strokePath()
}

// The Gate: two brackets [ ] + the operator dot between them.
let stroke = max(1.5, s * 0.075)
let armLen = s * 0.14
let bracketH = s * 0.46
let leftX = s * 0.30
let rightX = s * 0.70
let midY = s * 0.5

ctx.setStrokeColor(accent)
ctx.setLineWidth(stroke)
ctx.setLineCap(.round)
ctx.setLineJoin(.round)

// Left bracket [
ctx.beginPath()
ctx.move(to: CGPoint(x: leftX + armLen, y: midY + bracketH / 2))
ctx.addLine(to: CGPoint(x: leftX, y: midY + bracketH / 2))
ctx.addLine(to: CGPoint(x: leftX, y: midY - bracketH / 2))
ctx.addLine(to: CGPoint(x: leftX + armLen, y: midY - bracketH / 2))
ctx.strokePath()

// Right bracket ]
ctx.beginPath()
ctx.move(to: CGPoint(x: rightX - armLen, y: midY + bracketH / 2))
ctx.addLine(to: CGPoint(x: rightX, y: midY + bracketH / 2))
ctx.addLine(to: CGPoint(x: rightX, y: midY - bracketH / 2))
ctx.addLine(to: CGPoint(x: rightX - armLen, y: midY - bracketH / 2))
ctx.strokePath()

// The operator: a filled dot at the gate's center.
let dotR = s * 0.065
ctx.setFillColor(accent)
ctx.fillEllipse(in: CGRect(x: s / 2 - dotR, y: midY - dotR, width: dotR * 2, height: dotR * 2))

return ctx.makeImage()!
}

func writePNG(_ image: CGImage, to url: URL) {
let rep = NSBitmapImageRep(cgImage: image)
let data = rep.representation(using: .png, properties: [:])!
try! data.write(to: url)
}

let outDir = CommandLine.arguments.count > 1 ? CommandLine.arguments[1] : "app/icon"
let iconset = URL(fileURLWithPath: outDir).appendingPathComponent("episteme.iconset")
try? FileManager.default.createDirectory(at: iconset, withIntermediateDirectories: true)

// (filename base size, pixel size) pairs iconutil expects.
let entries: [(String, Int)] = [
("icon_16x16", 16), ("icon_16x16@2x", 32),
("icon_32x32", 32), ("icon_32x32@2x", 64),
("icon_128x128", 128), ("icon_128x128@2x", 256),
("icon_256x256", 256), ("icon_256x256@2x", 512),
("icon_512x512", 512), ("icon_512x512@2x", 1024),
]
for (name, px) in entries {
writePNG(drawIcon(size: px), to: iconset.appendingPathComponent("\(name).png"))
}

let task = Process()
task.executableURL = URL(fileURLWithPath: "/usr/bin/iconutil")
task.arguments = [
"-c", "icns", iconset.path,
"-o", URL(fileURLWithPath: outDir).appendingPathComponent("episteme.icns").path,
]
try! task.run()
task.waitUntilExit()
print("wrote \(outDir)/episteme.icns (exit \(task.terminationStatus))")
67 changes: 60 additions & 7 deletions scripts/build_app_pkg.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,11 @@ echo "==> cargo build --release (app shell v$VERSION)"

echo "==> assembling $APP"
rm -rf "$APP"
mkdir -p "$APP/Contents/MacOS"
mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources"
cp "$APP_DIR/target/release/episteme-app" "$APP/Contents/MacOS/episteme-app"
# Icon (E179): committed reproducible artifact; regenerate with
# swift app/icon/make_icns.swift app/icon
cp "$APP_DIR/icon/episteme.icns" "$APP/Contents/Resources/episteme.icns"
cat > "$APP/Contents/Info.plist" << PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
Expand All @@ -36,21 +39,71 @@ cat > "$APP/Contents/Info.plist" << PLIST
<key>CFBundleVersion</key><string>$VERSION</string>
<key>CFBundleShortVersionString</key><string>$VERSION</string>
<key>CFBundleExecutable</key><string>episteme-app</string>
<key>CFBundleIconFile</key><string>episteme</string>
<key>CFBundlePackageType</key><string>APPL</string>
<key>LSMinimumSystemVersion</key><string>12.0</string>
<key>NSHighResolutionCapable</key><true/>
</dict>
</plist>
PLIST

echo "==> pkgbuild (unsigned)"
pkgbuild \
--component "$APP" \
# AppleDouble hygiene (E176 review → E179 measured reality): xattr -cr +
# ditto --noextattr remove the REMOVABLE class (quarantine, FinderInfo,
# resource forks). com.apple.provenance is SIP-protected AND re-stamped by
# the OS on every local write, so local builds ALWAYS carry its 11-byte
# AppleDouble entries in the payload — unavoidable by construction, harmless
# metadata. Release artifacts come from CI (release-assets.yml) where this
# does not gate anything; do not chase the residue further.
xattr -cr "$APP" 2>/dev/null || true
STAGED="$DIST/.staging/Episteme.app"
rm -rf "$DIST/.staging"
mkdir -p "$DIST/.staging"
ditto --noextattr --norsrc --noacl "$APP" "$STAGED"

# Signing tiers (E179): credential-ready, inert without the operator's cert.
# EPISTEME_SIGN_APP_ID "Developer ID Application: Name (TEAMID)"
# EPISTEME_SIGN_INSTALLER_ID "Developer ID Installer: Name (TEAMID)"
# EPISTEME_NOTARY_PROFILE notarytool keychain profile name
# FIRST CHECK once a real cert exists: hardened runtime vs the wry webview —
# if the window comes up blank under --options runtime, add an entitlements
# plist (com.apple.security.cs.allow-jit) here.
TIER="unsigned (personal use)"
if [ -n "${EPISTEME_SIGN_APP_ID:-}" ]; then
echo "==> codesign (hardened runtime, on the STAGED copy — signing must
follow attribute-stripping, never precede it)"
codesign --force --deep --options runtime --timestamp \
--sign "$EPISTEME_SIGN_APP_ID" "$STAGED"
TIER="signed"
fi

echo "==> pkgbuild"
RAW_PKG="$DIST/Episteme-unsigned.pkg"
COPYFILE_DISABLE=1 pkgbuild \
--component "$STAGED" \
--install-location /Applications \
--identifier com.episteme.app \
--version "$VERSION" \
"$DIST/Episteme.pkg"
"$RAW_PKG"

if [ -n "${EPISTEME_SIGN_INSTALLER_ID:-}" ]; then
echo "==> productsign"
productsign --sign "$EPISTEME_SIGN_INSTALLER_ID" "$RAW_PKG" "$DIST/Episteme.pkg"
rm -f "$RAW_PKG"
TIER="signed installer"
else
mv "$RAW_PKG" "$DIST/Episteme.pkg"
fi

if [ -n "${EPISTEME_NOTARY_PROFILE:-}" ]; then
echo "==> notarytool submit (waits for Apple)"
xcrun notarytool submit "$DIST/Episteme.pkg" \
--keychain-profile "$EPISTEME_NOTARY_PROFILE" --wait
xcrun stapler staple "$DIST/Episteme.pkg"
TIER="notarized"
fi

echo "done:"
echo "done: tier=$TIER"
ls -lh "$DIST/Episteme.pkg"
echo "install: open dist/Episteme.pkg (unsigned — right-click → Open on first run)"
if [ "$TIER" = "unsigned (personal use)" ]; then
echo "install: open dist/Episteme.pkg (unsigned — right-click → Open on first run)"
fi
Loading