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
46 changes: 46 additions & 0 deletions .github/scripts/otter-adapt-upstream.sh
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,52 @@ preview_path.write_text(adapt_job(preview_path.read_text(), 'deploy', 'stage-pre
]))
PY

# Keep upstream's provisioning-profile path, but make it optional for Otter's
# existing Developer ID releases. Otter Accounts has no Clerk RP domain.
python3 - <<'PY'
from pathlib import Path

path = Path('.github/workflows/release-desktop.yml')
text = path.read_text()
if ' MACOS_PROVISIONING_PROFILE:\n' not in text:
text = text.replace(' AZURE_TENANT_ID:\n',
' MACOS_PROVISIONING_PROFILE:\n required: false\n AZURE_TENANT_ID:\n', 1)
if ' MACOS_PROVISIONING_PROFILE:' not in text:
text = text.replace(' APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }}\n',
' APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }}\n'
' MACOS_PROVISIONING_PROFILE: ${{ secrets.MACOS_PROVISIONING_PROFILE }}\n', 1)
text = text.replace(' T3CODE_CLERK_PASSKEY_RP_DOMAINS: ${{ vars.CLERK_PASSKEY_RP_DOMAINS }}\n', '')
if ' T3CODE_MACOS_PASSKEY_RP_DOMAINS:' not in text:
text = text.replace(' MACOS_PROVISIONING_PROFILE: ${{ secrets.MACOS_PROVISIONING_PROFILE }}\n',
' MACOS_PROVISIONING_PROFILE: ${{ secrets.MACOS_PROVISIONING_PROFILE }}\n'
' T3CODE_MACOS_PASSKEY_RP_DOMAINS: ${{ vars.MACOS_PASSKEY_RP_DOMAINS }}\n', 1)
start_anchor = ' if has_all "$CSC_LINK" "$CSC_KEY_PASSWORD" "$APPLE_API_KEY" "$APPLE_API_KEY_ID" "$APPLE_API_ISSUER"; then\n'
end_anchor = ' echo "macOS signing enabled."\n'
start = text.index(start_anchor) + len(start_anchor)
end = text.index(end_anchor, start)
text = text[:start] + ''' key_path="$RUNNER_TEMP/AuthKey_${APPLE_API_KEY_ID}.p8"
printf '%s' "$APPLE_API_KEY" > "$key_path"
export APPLE_API_KEY="$key_path"

if [[ -n "$MACOS_PROVISIONING_PROFILE" ]]; then
if [[ -z "$APPLE_TEAM_ID" ]]; then
echo "MACOS_PROVISIONING_PROFILE requires APPLE_TEAM_ID." >&2
exit 1
fi
profile_path="$RUNNER_TEMP/t3code.provisionprofile"
printf '%s' "$MACOS_PROVISIONING_PROFILE" | base64 -D > "$profile_path"
security cms -D -i "$profile_path" >/dev/null
export T3CODE_APPLE_TEAM_ID="$APPLE_TEAM_ID"
export T3CODE_MACOS_PROVISIONING_PROFILE="$profile_path"
echo "macOS provisioning profile validated; entitlements selected by the packager."
else
echo "No macOS provisioning profile; signing and notarization continue without passkey entitlements."
fi

''' + text[end:]
path.write_text(text)
PY

# Point agents at the fork guide from AGENTS.md, which every agent reads first.
perl -0pi -e '
s/\A(# [^\n]*\n)/$1\n> **This is the Otter Code fork.** Read [OTTER.md](OTTER.md) first: it covers how the fork\n> stays in sync with upstream, its identities and infrastructure, releases, and moving machines.\n/
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/mobile-fingerprint-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@ jobs:
env:
BASE_SHA: ${{ steps.changes.outputs.base_sha }}
run: |
git checkout --quiet "$BASE_SHA"
# Filtered installs can rewrite tracked dependency metadata. Fingerprint
# artifacts are in RUNNER_TEMP; discard install edits in this CI checkout.
git checkout --quiet --force "$BASE_SHA"
# Re-sync node_modules to the base commit's lockfile before
# fingerprinting — a dep-changing PR must not fingerprint the base
# against head's installed packages.
Expand Down
19 changes: 19 additions & 0 deletions .github/workflows/release-desktop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ on:
required: false
APPLE_API_ISSUER:
required: false
MACOS_PROVISIONING_PROFILE:
required: false
AZURE_TENANT_ID:
required: false
AZURE_CLIENT_ID:
Expand Down Expand Up @@ -409,6 +411,8 @@ jobs:
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }}
MACOS_PROVISIONING_PROFILE: ${{ secrets.MACOS_PROVISIONING_PROFILE }}
T3CODE_MACOS_PASSKEY_RP_DOMAINS: ${{ vars.MACOS_PASSKEY_RP_DOMAINS }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
Expand Down Expand Up @@ -441,6 +445,21 @@ jobs:
printf '%s' "$APPLE_API_KEY" > "$key_path"
export APPLE_API_KEY="$key_path"

if [[ -n "$MACOS_PROVISIONING_PROFILE" ]]; then
if [[ -z "$APPLE_TEAM_ID" ]]; then
echo "MACOS_PROVISIONING_PROFILE requires APPLE_TEAM_ID." >&2
exit 1
fi
profile_path="$RUNNER_TEMP/t3code.provisionprofile"
printf '%s' "$MACOS_PROVISIONING_PROFILE" | base64 -D > "$profile_path"
security cms -D -i "$profile_path" >/dev/null
export T3CODE_APPLE_TEAM_ID="$APPLE_TEAM_ID"
export T3CODE_MACOS_PROVISIONING_PROFILE="$profile_path"
echo "macOS provisioning profile validated; entitlements selected by the packager."
else
echo "No macOS provisioning profile; signing and notarization continue without passkey entitlements."
fi

echo "macOS signing enabled."
args+=(--signed)
else
Expand Down
18 changes: 14 additions & 4 deletions OTTER.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ sync_date=$(date +%F)
git merge-base --is-ancestor "$old_base" "$old_main"
git merge-base --is-ancestor "$old_base" "$new_base"
last_sync=$(git log -1 --format=%H --grep='^chore(otter): changes before the .* sync' "$old_main")
git branch "backup/otter-before-$sync_date" "$old_main"
# Commits since the last sync wait here; they become this sync's squash commit.
git branch "otter/sync-$sync_date" "$old_main"
git switch -c "otter/stack-$sync_date" "$last_sync"
Expand Down Expand Up @@ -103,12 +102,12 @@ Run focused tests and typechecks for conflicted code and the preserved features

### Publishing a prepared sync

A sync rewrites Otter's shared history. Preserve the old `main` as a remote backup, then update
`main` and `upstream-base` together with explicit leases. If another writer moved either ref, stop
A sync rewrites Otter's shared history. Update `main` and `upstream-base` together with explicit
leases; the squash commits are the record of the fork's changes, so no backup branches are kept.
If another writer moved either ref, stop
and prepare against the new state instead of overriding it:

```sh
git push origin "$old_main:refs/heads/backup/otter-before-$sync_date"
git push --atomic origin \
--force-with-lease="refs/heads/main:$old_main" \
--force-with-lease="refs/heads/upstream-base:$old_base" \
Expand Down Expand Up @@ -183,6 +182,17 @@ widget's `T3Mark.svg`, `agent_activity_mark.xml`, the DMG backgrounds) is the ki
packages with the `NPM_TOKEN` secret (an npm account that is a member of the `otterware` org),
tagged `nightly` and also `latest`, so `npx otter-code` and `npm i -g otter-code` get the newest
build. The token is needed because new packages have no npm trusted publisher yet.
- **macOS passkeys:** `MACOS_PROVISIONING_PROFILE` is the base64 Developer ID profile for
`dev.otterware.code`, signed by Clary GmbH's team `YNJ5WLH965` (`APPLE_TEAM_ID`). The
Otterware branch must pass its own `OTTERWARE_MACOS_PROVISIONING_PROFILE` secret into the
reusable workflow's `MACOS_PROVISIONING_PROFILE` input and use app ID `dev.otterware.suite`.
The packager derives the WebAuthn keychain group from the team and app IDs and claims only
used profile grants. Apple's managed `com.apple.developer.web-browser.public-key-credential`
capability is pending approval; after approval, regenerate both profiles and update their
secrets. Associated Domains are optional (`MACOS_PASSKEY_RP_DOMAINS`, comma-separated) and
unnecessary for browser passkeys; no Clerk key is required. Without a profile, existing
signing and notarization continue. Otter's policy lives in `scripts/otter/macos-profile.ts`;
regenerate workflow adaptations with `.github/scripts/otter-adapt-upstream.sh`.
- **Hosted web:** the release stages a Cloudflare Worker version, then promotes it after the
desktop/CLI release publishes. `scripts/otter/web/` owns its routing and static assets;
the adaptation script keeps upstream's Vercel path separate. The `Deploy Otter Code web`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ final class T3LayoutMetricsView: ExpoView {
}
#if compiler(>=6.4)
if #available(iOS 27.1, *) {
hasHinge = nil
// Publish as hinge-less right away: phones without a hinge may never get an update,
// and the workspace waits for metrics before attaching any screen.
addInteraction(UIHingeInteraction { [weak self] _, update in
guard let self, self.window != nil else { return }
// Capability survives temporary detachment and does not follow the hinge angle.
Expand All @@ -35,21 +36,9 @@ final class T3LayoutMetricsView: ExpoView {
override func didMoveToWindow() {
super.didMoveToWindow()
lastMetrics = nil
assumeNoHingeIfSilent()
publishMetrics()
}

/// UIKit only sends hinge updates on devices that have a hinge. Without this,
/// an ordinary iPhone never publishes and the navigator never attaches.
private func assumeNoHingeIfSilent() {
guard window != nil, hasHinge == nil else { return }
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak self] in
guard let self, self.window != nil, self.hasHinge == nil else { return }
self.hasHinge = false
self.publishMetrics()
}
}

override func layoutSubviews() {
super.layoutSubviews()
publishMetrics()
Expand Down
8 changes: 8 additions & 0 deletions docs/internals/server-updates.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ their service definition or select their own replacement. Local service commands
may replace the launcher and state while the service is stopped. Foreground CLI
processes do not self-update.

SSH launch defers to service ownership: durable `runtime/service-state.json`
(including pending updates or invalid state), an installed Otter systemd unit or
launchd plist, or an enabled `otter-code.service` prevents an SSH-managed spawn.
It waits up to 120 seconds for the default home's runtime metadata and HTTP
readiness, then connects as external. A service restart gap must never create a
second writer on the same database. If the service stays down, SSH fails with
service recovery commands instead of taking over the home.

Exact-version installs keep restarts independent of npm cache eviction or a moving
release tag. Installation and preflight happen in staging before publishing an
immutable runtime. Preflight checks the launcher protocol because a target that
Expand Down
17 changes: 10 additions & 7 deletions docs/operations/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -524,23 +524,26 @@ Required secrets used by the workflow:
- `APPLE_API_KEY`
- `APPLE_API_KEY_ID`
- `APPLE_API_ISSUER`
- `MACOS_PROVISIONING_PROFILE` (base64-encoded provisioning profile with Associated Domains)

Required repository variables:

- `APPLE_TEAM_ID`

Optional repository variables:

domain from the production Otter Accounts issuer.
- `MACOS_PASSKEY_RP_DOMAINS`: explicitly configured, comma-separated Associated Domains.
Browser passkeys need none. The profile must authorize each configured domain.

Optional secret: `MACOS_PROVISIONING_PROFILE`, a base64-encoded Developer ID profile. Without
it, signing and notarization continue with the existing hardened-runtime entitlements.

Checklist:

1. Apple Developer account access:
- Team has rights to create Developer ID certificates.
2. Create an explicit App ID for `com.t3tools.t3code` and enable Associated Domains.
2. Create an explicit App ID for `dev.otterware.code`; enable Associated Domains only if used.
3. Create a `Developer ID Application` certificate and a compatible provisioning profile for that
App ID with Associated Domains enabled.
App ID with the passkey capabilities Apple has granted (see [OTTER.md](../../OTTER.md)).
4. Export the certificate + private key as `.p12` from Keychain.
5. Base64-encode the `.p12` and store as `CSC_LINK`.
6. Base64-encode the provisioning profile and store it as `MACOS_PROVISIONING_PROFILE`.
Expand All @@ -551,8 +554,8 @@ Checklist:
- `APPLE_API_KEY`: contents of the downloaded `.p8`
- `APPLE_API_KEY_ID`: Key ID
- `APPLE_API_ISSUER`: Issuer ID
10. Re-run a tag release and confirm macOS artifacts are signed/notarized and contain the expected
`com.apple.developer.associated-domains` entitlement.
10. Re-run a release and confirm macOS artifacts are signed/notarized and contain only the
configured, profile-authorized passkey entitlements reported by the packager.

Notes:

Expand All @@ -563,7 +566,7 @@ Notes:
- In-app browser passkeys depend on the same profile. The packager adds each entitlement only when
the profile grants it, because macOS will not launch an app that claims more than its profile
allows. The build log reports which ones it enabled.
- `keychain-access-groups` (`<TEAM_ID>.com.t3tools.t3code.webauthn`) enables Touch ID passkeys.
- `keychain-access-groups` (`<TEAM_ID>.<APP_ID>.webauthn`) enables Touch ID passkeys.
Profiles that grant the team's keychain groups (`<TEAM_ID>.*`) cover it.
- `com.apple.developer.web-browser.public-key-credential` lets the system passkey sheet (iCloud
Keychain, password managers, phones, security keys) serve any site. Apple grants it as a
Expand Down
Loading
Loading