Compare commits

..
Author SHA1 Message Date
buzzert 730d609f81 Relax Xcode signing identity matching
TestFlight / testflight (push) Failing after 26s
2026-06-25 23:57:17 -07:00
buzzert 137fce8558 Use passworded CI keychain
TestFlight / testflight (push) Failing after 25s
2026-06-25 23:53:24 -07:00
buzzert 3e6d3c6817 Use user keychain domain in CI
TestFlight / testflight (push) Failing after 23s
2026-06-25 23:51:33 -07:00
buzzert 100b51de12 Use explicit CI signing keychain
TestFlight / testflight (push) Failing after 18s
2026-06-25 23:48:26 -07:00
buzzert 23ee30a53a Simplify TestFlight CI signing
TestFlight / testflight (push) Failing after 22s
2026-06-25 23:44:13 -07:00
buzzert 0be2442ad0 Pass signing keychain to Xcode resolver
TestFlight / testflight (push) Failing after 28s
2026-06-25 23:37:24 -07:00
buzzert c84ef8c242 Refresh CI key partition access before build
TestFlight / testflight (push) Failing after 27s
2026-06-25 23:35:00 -07:00
buzzert 98f96eda45 Let Xcode select Apple Distribution identity
TestFlight / testflight (push) Failing after 25s
2026-06-25 23:32:38 -07:00
buzzert 3904457c21 Use runner home for CI keychain preferences
TestFlight / testflight (push) Failing after 24s
2026-06-25 23:30:46 -07:00
buzzert 0fc2117a11 Set CI keychain as default for Xcode
TestFlight / testflight (push) Failing after 18s
2026-06-25 23:28:58 -07:00
buzzert 60469f05b5 Tolerate login keychain preference failure
TestFlight / testflight (push) Failing after 23s
2026-06-25 23:27:13 -07:00
buzzert d834ed7931 Create CI login keychain when missing
TestFlight / testflight (push) Failing after 19s
2026-06-25 23:25:42 -07:00
buzzert f98a002f52 Use explicit runner login keychain
TestFlight / testflight (push) Failing after 17s
2026-06-25 23:23:13 -07:00
buzzert b0c0a2d55e Reset CI keychain search list
TestFlight / testflight (push) Failing after 19s
2026-06-25 23:21:42 -07:00
buzzert 3262f4ff80 Detect runner login keychain path
TestFlight / testflight (push) Failing after 19s
2026-06-25 23:20:06 -07:00
buzzert 585be09eb7 Target login keychain path for CI signing
TestFlight / testflight (push) Failing after 19s
2026-06-25 23:18:01 -07:00
buzzert 387896741c Use runner login keychain for CI signing
TestFlight / testflight (push) Failing after 21s
2026-06-25 23:16:22 -07:00
buzzert f6a10af7a9 Use signing certificate identity hash
TestFlight / testflight (push) Failing after 24s
2026-06-25 23:13:08 -07:00
buzzert 8aab86e2a6 Avoid changing default keychain in CI
TestFlight / testflight (push) Failing after 25s
2026-06-25 23:10:35 -07:00
buzzert eb4b233e33 Resolve CI signing keychain path
TestFlight / testflight (push) Failing after 18s
2026-06-25 23:08:35 -07:00
buzzert cbd7a68e57 Make CI signing keychain visible to Xcode
TestFlight / testflight (push) Failing after 21s
2026-06-25 23:06:00 -07:00
buzzert 04c15e8f12 Use absolute iOS paths in Fastlane
TestFlight / testflight (push) Failing after 25s
2026-06-25 22:50:30 -07:00
buzzert ca28ebc0a0 Use disposable match keychain in CI
TestFlight / testflight (push) Failing after 16s
2026-06-25 22:48:59 -07:00
buzzert 87787642b5 Preserve Ruby path for TestFlight workflow
TestFlight / testflight (push) Failing after 22s
2026-06-25 22:46:14 -07:00
buzzert 4124a31a34 Use Ruby 3.1 for TestFlight workflow
TestFlight / testflight (push) Failing after 21s
2026-06-25 22:43:27 -07:00
buzzert a68f1e50ca Reset iOS TestFlight deployment
TestFlight / testflight (push) Failing after 14s
2026-06-25 22:41:00 -07:00
buzzert 272ad0bbf0 ios: pass signing settings to archive
TestFlight Release / testflight (push) Failing after 17s
2026-06-25 22:19:25 -07:00
buzzert de7b448bc5 ios: avoid system default keychain writes
TestFlight Release / testflight (push) Failing after 16s
2026-06-25 22:16:24 -07:00
buzzert 3c7fc51fdb ios: set ci keychain in default domain
TestFlight Release / testflight (push) Failing after 10s
2026-06-25 22:14:25 -07:00
buzzert 0062f37b9f ios: sign with disposable login keychain
TestFlight Release / testflight (push) Failing after 17s
2026-06-25 22:12:17 -07:00
buzzert 0ae551615f ios: use signing identity fingerprint in ci
TestFlight Release / testflight (push) Failing after 16s
2026-06-25 22:10:06 -07:00
buzzert 88bef50ae7 ios: create named ci keychain in home
TestFlight Release / testflight (push) Failing after 15s
2026-06-25 22:07:12 -07:00
buzzert 0d069b4233 ios: create ci keychain by name
TestFlight Release / testflight (push) Failing after 11s
2026-06-25 22:05:47 -07:00
buzzert 60bbe077e8 ios: pass signing keychain to xcode
TestFlight Release / testflight (push) Failing after 18s
2026-06-25 22:02:19 -07:00
buzzert 0b09d5425b ios: handle empty ci keychain list
TestFlight Release / testflight (push) Failing after 15s
2026-06-25 21:58:01 -07:00
buzzert c9a3015e35 ios: parse ci profile without keychain
TestFlight Release / testflight (push) Failing after 9s
2026-06-25 21:56:19 -07:00
buzzert abd8a80daa ios: isolate ci signing keychains
TestFlight Release / testflight (push) Failing after 8s
2026-06-25 21:52:17 -07:00
buzzert 0f76ef91a9 ios: restore working ci p12 import
TestFlight Release / testflight (push) Failing after 9s
2026-06-25 21:48:19 -07:00
buzzert 72e2ffd898 ios: use temporary keychain path in ci
TestFlight Release / testflight (push) Failing after 9s
2026-06-25 21:46:48 -07:00
buzzert 4c610c89e1 ios: install ci profiles for xcode signing
TestFlight Release / testflight (push) Failing after 9s
2026-06-25 21:44:42 -07:00
buzzert 477921563f ios: remove invalid ci codesign path
TestFlight Release / testflight (push) Failing after 18s
2026-06-25 21:36:37 -07:00
buzzert 0fca0e93ec ios: grant ci key access to xcode tools
TestFlight Release / testflight (push) Failing after 10s
2026-06-25 21:35:11 -07:00
buzzert f977f9943c ios: patch generated release signing settings
TestFlight Release / testflight (push) Failing after 16s
2026-06-25 21:31:51 -07:00
buzzert f445730a41 ios: override iphoneos signing identity
TestFlight Release / testflight (push) Failing after 16s
2026-06-25 21:29:35 -07:00
buzzert 76cb808c33 ios: use disposable keychain as ci default
TestFlight Release / testflight (push) Failing after 15s
2026-06-25 21:27:19 -07:00
buzzert e167bd983f ios: use generic xcode signing selector
TestFlight Release / testflight (push) Failing after 19s
2026-06-25 21:25:13 -07:00
buzzert e4dd91564f ios: unlock signing keychain before build
TestFlight Release / testflight (push) Failing after 17s
2026-06-25 21:20:31 -07:00
buzzert 3bfde476a6 ios: use single identity signing p12
TestFlight Release / testflight (push) Failing after 16s
2026-06-25 21:18:54 -07:00
buzzert b8676027db ios: trust Apple root in CI signing keychain
TestFlight Release / testflight (push) Failing after 8s
2026-06-25 21:12:53 -07:00
buzzert d36d2c60a3 ios: install Apple WWDR intermediate in CI
TestFlight Release / testflight (push) Failing after 18s
2026-06-25 21:11:01 -07:00
buzzert 3d7031bb40 ios: avoid default keychain mutation in ci
TestFlight Release / testflight (push) Failing after 17s
2026-06-25 21:08:32 -07:00
buzzert fa9b725c77 ios: expose signing keychain to xcodebuild
TestFlight Release / testflight (push) Failing after 9s
2026-06-25 21:07:38 -07:00
buzzert a88987d08d ios: pin distribution signing identity
TestFlight Release / testflight (push) Failing after 15s
2026-06-25 21:05:26 -07:00
buzzert e137ea1077 ios: bootstrap signing with existing certificate
TestFlight Release / testflight (push) Failing after 17s
2026-06-25 21:03:43 -07:00
buzzert fad25d7f2b ios: configure api-key TestFlight signing 2026-06-25 20:51:01 -07:00
buzzert fb28508764 ios: ci: keychain cleanup 2026-06-25 20:35:39 -07:00
buzzert 4365798f5e workflow: fix
TestFlight Release / testflight (push) Failing after 16s
2026-06-25 20:21:39 -07:00
8 changed files with 139 additions and 229 deletions
+20 -2
View File
@@ -4,7 +4,7 @@ on:
workflow_dispatch: workflow_dispatch:
push: push:
tags: tags:
- "release/ios/v*" - "v*"
jobs: jobs:
testflight: testflight:
@@ -33,6 +33,25 @@ jobs:
brew install xcodegen brew install xcodegen
fi fi
- name: Prepare Runner Keychain
env:
HOME: /var/lib/act_runner
run: |
set -euo pipefail
mkdir -p "${HOME}/Library/Keychains"
login_keychain="${HOME}/Library/Keychains/login.keychain"
if [ ! -f "${login_keychain}-db" ]; then
security create-keychain -p "" "${login_keychain}"
fi
security unlock-keychain -p "" "${login_keychain}" 2>/dev/null || \
security unlock-keychain -p "sybil-ci-keychain-password" "${login_keychain}" 2>/dev/null || true
security default-keychain -d user -s "${login_keychain}"
security list-keychains -d user -s "${login_keychain}-db"
security delete-keychain "${HOME}/Library/Keychains/sybil_ci_keychain" >/dev/null 2>&1 || true
rm -f "${HOME}/Library/Keychains/sybil_ci_keychain" "${HOME}/Library/Keychains/sybil_ci_keychain-db"
- name: Upload to TestFlight - name: Upload to TestFlight
working-directory: ios working-directory: ios
env: env:
@@ -43,7 +62,6 @@ jobs:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_GIT_URL: ${{ secrets.MATCH_GIT_URL }} MATCH_GIT_URL: ${{ secrets.MATCH_GIT_URL }}
MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }} MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }}
SYBIL_BUILD_NUMBER: ${{ github.run_number }}
FASTLANE_SKIP_UPDATE_CHECK: "1" FASTLANE_SKIP_UPDATE_CHECK: "1"
FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: "120" FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: "120"
run: | run: |
-1
View File
@@ -285,7 +285,6 @@ Behavior notes:
- For `chatId` calls, server stores only *new* non-assistant messages from provided history to avoid duplicates. - For `chatId` calls, server stores only *new* non-assistant messages from provided history to avoid duplicates.
- `additionalSystemPrompt`, when present directly or loaded from stored chat settings, is prepended to the provider request as a `system` message and is not inserted into the persisted chat transcript by this endpoint. - `additionalSystemPrompt`, when present directly or loaded from stored chat settings, is prepended to the provider request as a `system` message and is not inserted into the persisted chat transcript by this endpoint.
- `enabledTools` limits Sybil-managed tools for this request. When omitted for a saved chat, the stored chat setting is used; otherwise all available tools are enabled by default. An empty array disables Sybil-managed tools. - `enabledTools` limits Sybil-managed tools for this request. When omitted for a saved chat, the stored chat setting is used; otherwise all available tools are enabled by default. An empty array disables Sybil-managed tools.
- `maxTokens` is optional. For `anthropic`, when omitted the backend requests the selected model's maximum output token limit from Anthropic's Models API and uses that as `max_tokens`; if the model limit cannot be loaded, the fallback is 128000. For other providers, omitted `maxTokens` is not sent as an explicit cap.
- Server persists final assistant output and call metadata (`LlmCall`) in DB. - Server persists final assistant output and call metadata (`LlmCall`) in DB.
- Server updates chat-level model metadata on each call: `lastUsedProvider`/`lastUsedModel`; first successful/failed call also initializes `initiatedProvider`/`initiatedModel` if unset. - Server updates chat-level model metadata on each call: `lastUsedProvider`/`lastUsedModel`; first successful/failed call also initializes `initiatedProvider`/`initiatedModel` if unset.
- Attachments are optional and currently apply to `user` messages. Persisted chat history stores them under `message.metadata.attachments`. - Attachments are optional and currently apply to `user` messages. Persisted chat history stores them under `message.metadata.attachments`.
-1
View File
@@ -64,7 +64,6 @@ Notes:
- For persisted streams, backend stores only new non-assistant input history rows to avoid duplicates. - For persisted streams, backend stores only new non-assistant input history rows to avoid duplicates.
- `additionalSystemPrompt`, when present directly or loaded from stored chat settings, is prepended to the provider request as a `system` message and is not inserted into the persisted chat transcript by this endpoint. - `additionalSystemPrompt`, when present directly or loaded from stored chat settings, is prepended to the provider request as a `system` message and is not inserted into the persisted chat transcript by this endpoint.
- `enabledTools` limits Sybil-managed tools for this request. When omitted for a saved chat, the stored chat setting is used; otherwise all available tools are enabled by default. An empty array disables Sybil-managed tools. - `enabledTools` limits Sybil-managed tools for this request. When omitted for a saved chat, the stored chat setting is used; otherwise all available tools are enabled by default. An empty array disables Sybil-managed tools.
- `maxTokens` is optional. For `anthropic`, when omitted the backend requests the selected model's maximum output token limit from Anthropic's Models API and uses that as `max_tokens`; if the model limit cannot be loaded, the fallback is 128000. For other providers, omitted `maxTokens` is not sent as an explicit cap.
- Attachments are optional and are persisted under `message.metadata.attachments` on stored user messages when `persist` is `true`. - Attachments are optional and are persisted under `message.metadata.attachments` on stored user messages when `persist` is `true`.
Persisted chat streams with a `chatId` are backend-owned active runs: Persisted chat streams with a `chatId` are backend-owned active runs:
+1 -1
View File
@@ -24,7 +24,7 @@ targets:
GENERATE_INFOPLIST_FILE: YES GENERATE_INFOPLIST_FILE: YES
INFOPLIST_FILE: Apps/Sybil/Info.plist INFOPLIST_FILE: Apps/Sybil/Info.plist
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
MARKETING_VERSION: "1.13.2" MARKETING_VERSION: "1.10"
CURRENT_PROJECT_VERSION: 11 CURRENT_PROJECT_VERSION: 11
INFOPLIST_KEY_CFBundleDisplayName: Sybil INFOPLIST_KEY_CFBundleDisplayName: Sybil
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption: NO INFOPLIST_KEY_ITSAppUsesNonExemptEncryption: NO
+113 -61
View File
@@ -1,3 +1,4 @@
require "fileutils"
require "shellwords" require "shellwords"
default_platform(:ios) default_platform(:ios)
@@ -6,59 +7,41 @@ APP_IDENTIFIER = "net.buzzert.sybil2"
SCHEME = "Sybil" SCHEME = "Sybil"
TEAM_ID = "DQQH5H6GBD" TEAM_ID = "DQQH5H6GBD"
PROFILE_NAME = "Sybil AppStore CI" PROFILE_NAME = "Sybil AppStore CI"
SIGNING_IDENTITY = "Apple Distribution: James Magahern (DQQH5H6GBD)"
CI_KEYCHAIN_NAME = "sybil_ci_keychain" CI_KEYCHAIN_NAME = "sybil_ci_keychain"
CI_KEYCHAIN_PASSWORD = "sybil-ci-keychain-password" CI_KEYCHAIN_PASSWORD = "sybil-ci-keychain-password"
CI_KEYCHAIN_DB_PATH = File.expand_path("~/Library/Keychains/#{CI_KEYCHAIN_NAME}-db")
IOS_ROOT = File.expand_path("..", __dir__) IOS_ROOT = File.expand_path("..", __dir__)
PROJECT_FILE = File.join(IOS_ROOT, "Sybil.xcodeproj") PROJECT_FILE = File.join(IOS_ROOT, "Sybil.xcodeproj")
PROJECT_SPEC = File.join(IOS_ROOT, "project.yml") PROJECT_SPEC = File.join(IOS_ROOT, "project.yml")
APP_PROJECT_SPEC = File.join(IOS_ROOT, "Apps/Sybil/project.yml") CI_KEYCHAIN_PATH = File.join(File.expand_path("~/Library/Keychains"), CI_KEYCHAIN_NAME)
CI_KEYCHAIN_DB_PATH = "#{CI_KEYCHAIN_PATH}-db"
LOGIN_KEYCHAIN_PATH = File.expand_path("~/Library/Keychains/login.keychain")
LOGIN_KEYCHAIN_DB_PATH = "#{LOGIN_KEYCHAIN_PATH}-db"
def present?(value) def present?(value)
!value.to_s.strip.empty? !value.to_s.strip.empty?
end end
def release_version
tag = ENV["SYBIL_VERSION_TAG"].to_s
tag = ENV["GITHUB_REF_NAME"].to_s if !present?(tag)
tag = ENV["GITHUB_REF"].to_s.sub(%r{\Arefs/tags/}, "") if !present?(tag)
tag = sh("git describe --tags --abbrev=0").strip if !present?(tag)
version = tag.sub(%r{\Arelease/}, "").sub(/\Av/, "")
unless version.match?(/\A\d+\.\d+\.\d+\z/)
UI.user_error!("Release tag must look like v1.2.3; got #{tag.inspect}")
end
version
end
def ci? def ci?
present?(ENV["CI"]) present?(ENV["CI"])
end end
def release_version def ci_keychain_path
tag = ENV["SYBIL_VERSION_TAG"] File.file?(CI_KEYCHAIN_DB_PATH) ? CI_KEYCHAIN_DB_PATH : CI_KEYCHAIN_PATH
tag = ENV["GITHUB_REF_NAME"] if !present?(tag)
tag = ENV["GITHUB_REF"].to_s.sub(%r{\Arefs/tags/}, "") if !present?(tag)
tag = sh("git describe --tags --abbrev=0").strip if !present?(tag)
match = tag.to_s.match(%r{\Arelease/ios/v(\d+\.\d+\.\d+)\z})
unless match
UI.user_error!("Release tag must look like release/ios/v1.2.3; got #{tag.inspect}")
end
match[1]
end
# App Store Connect requires CFBundleVersion to be unique and strictly
# increasing app-wide (not just per marketing version), so we derive it from
# the monotonic CI run number rather than querying TestFlight (that query can
# lag behind builds still processing and hand back a colliding value).
def build_number
value = present?(ENV["SYBIL_BUILD_NUMBER"]) ? ENV["SYBIL_BUILD_NUMBER"] : ENV["GITHUB_RUN_NUMBER"]
unless value.to_s.match?(/\A\d+\z/)
UI.user_error!("Build number must come from SYBIL_BUILD_NUMBER/GITHUB_RUN_NUMBER; got #{value.inspect}")
end
value.to_i
end
def stamp_marketing_version(version)
contents = File.read(APP_PROJECT_SPEC)
updated = contents.sub(/^(\s*MARKETING_VERSION:\s*).*/, "\\1\"#{version}\"")
if updated == contents
UI.user_error!("Could not find MARKETING_VERSION in #{APP_PROJECT_SPEC}")
end
File.write(APP_PROJECT_SPEC, updated)
end end
platform :ios do platform :ios do
@@ -71,27 +54,54 @@ platform :ios do
) )
end end
# CI has no login keychain, so create a dedicated throwaway one for match to private_lane :setup_ci_signing do
# import the distribution cert into. The runner's launchd job sets
# SessionCreate, so add_to_search_list actually makes it visible to xcodebuild.
private_lane :prepare_ci_keychain do
next unless ci? next unless ci?
delete_keychain(name: CI_KEYCHAIN_NAME) if File.file?(CI_KEYCHAIN_DB_PATH) FileUtils.mkdir_p(File.dirname(CI_KEYCHAIN_PATH))
sh("security delete-keychain #{CI_KEYCHAIN_PATH.shellescape} || true", log: false)
FileUtils.rm_f(CI_KEYCHAIN_PATH)
FileUtils.rm_f(CI_KEYCHAIN_DB_PATH)
create_keychain( create_keychain(
name: CI_KEYCHAIN_NAME, path: CI_KEYCHAIN_PATH,
password: CI_KEYCHAIN_PASSWORD, password: CI_KEYCHAIN_PASSWORD,
default_keychain: false,
unlock: true, unlock: true,
timeout: 3600, timeout: 3600,
add_to_search_list: true lock_when_sleeps: true,
add_to_search_list: false
) )
ENV["MATCH_KEYCHAIN_NAME"] = CI_KEYCHAIN_NAME sh("security default-keychain -d user -s #{CI_KEYCHAIN_PATH.shellescape}", log: false)
sh("security list-keychains -d user -s #{ci_keychain_path.shellescape}", log: false)
sh("security list-keychains -d dynamic -s #{ci_keychain_path.shellescape} || true", log: false)
sh("security list-keychains -d common -s #{ci_keychain_path.shellescape} || true", log: false)
ENV["MATCH_KEYCHAIN_NAME"] = CI_KEYCHAIN_PATH
ENV["MATCH_KEYCHAIN_PASSWORD"] = CI_KEYCHAIN_PASSWORD ENV["MATCH_KEYCHAIN_PASSWORD"] = CI_KEYCHAIN_PASSWORD
ENV["MATCH_READONLY"] = "true"
end
private_lane :cleanup_ci_signing do
next unless ci?
if File.file?(LOGIN_KEYCHAIN_DB_PATH) || File.file?(LOGIN_KEYCHAIN_PATH)
sh("security default-keychain -d user -s #{LOGIN_KEYCHAIN_PATH.shellescape} || true", log: false)
sh("security list-keychains -d user -s #{LOGIN_KEYCHAIN_DB_PATH.shellescape} || true", log: false)
end
sh("security delete-keychain #{ci_keychain_path.shellescape} || true", log: false)
FileUtils.rm_f(CI_KEYCHAIN_PATH)
FileUtils.rm_f(CI_KEYCHAIN_DB_PATH)
rescue => error
UI.message("Unable to delete temporary CI keychain: #{error.message}")
ensure
ENV.delete("MATCH_KEYCHAIN_NAME")
ENV.delete("MATCH_KEYCHAIN_PASSWORD")
ENV.delete("MATCH_READONLY")
end end
private_lane :sync_signing do |options| private_lane :sync_signing do |options|
match( match_options = {
type: "appstore", type: "appstore",
readonly: options.fetch(:readonly), readonly: options.fetch(:readonly),
app_identifier: APP_IDENTIFIER, app_identifier: APP_IDENTIFIER,
@@ -102,7 +112,28 @@ platform :ios do
git_full_name: "Sybil Release Bot", git_full_name: "Sybil Release Bot",
git_user_email: "james.magahern@me.com", git_user_email: "james.magahern@me.com",
api_key: options.fetch(:api_key) api_key: options.fetch(:api_key)
) }
match_options[:keychain_name] = ENV["MATCH_KEYCHAIN_NAME"] if present?(ENV["MATCH_KEYCHAIN_NAME"])
match_options[:keychain_password] = ENV["MATCH_KEYCHAIN_PASSWORD"] if ENV.key?("MATCH_KEYCHAIN_PASSWORD")
match(match_options)
end
private_lane :verify_ci_signing do
next unless ci?
if File.file?(ci_keychain_path)
password = ENV.fetch("MATCH_KEYCHAIN_PASSWORD", "")
sh("security unlock-keychain -p #{password.shellescape} #{ci_keychain_path.shellescape}", log: false)
sh("security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k #{password.shellescape} #{ci_keychain_path.shellescape}", log: false)
end
identities = sh("security find-identity -v -p codesigning #{ci_keychain_path.shellescape}", log: false)
UI.message(identities)
unless identities.include?(SIGNING_IDENTITY)
UI.user_error!("The CI keychain search list does not contain #{SIGNING_IDENTITY}")
end
end end
desc "Create or update match signing assets" desc "Create or update match signing assets"
@@ -112,30 +143,49 @@ platform :ios do
desc "Build and upload to TestFlight" desc "Build and upload to TestFlight"
lane :beta do lane :beta do
prepare_ci_keychain setup_ci_signing
api_key = app_store_api_key api_key = app_store_api_key
version = release_version sh("xcodegen --spec #{PROJECT_SPEC.shellescape}")
stamp_marketing_version(version)
sh("xcodegen", "--spec", PROJECT_SPEC)
increment_version_number(version_number: version, xcodeproj: PROJECT_FILE) increment_version_number(
increment_build_number(build_number: build_number, xcodeproj: PROJECT_FILE) version_number: release_version,
xcodeproj: PROJECT_FILE
)
latest_build_number = latest_testflight_build_number(
app_identifier: APP_IDENTIFIER,
api_key: api_key,
initial_build_number: 0
)
increment_build_number(
build_number: latest_build_number + 1,
xcodeproj: PROJECT_FILE
)
sync_signing(api_key: api_key, readonly: true) sync_signing(api_key: api_key, readonly: true)
verify_ci_signing
xcargs = [
"DEVELOPMENT_TEAM=#{TEAM_ID.shellescape}",
"CODE_SIGN_STYLE=Manual",
"CODE_SIGN_IDENTITY=Apple\\ Distribution",
"PROVISIONING_PROFILE_SPECIFIER=#{PROFILE_NAME.shellescape}"
]
if ci?
xcargs << "CODE_SIGN_KEYCHAIN=#{ci_keychain_path.shellescape}"
xcargs << "OTHER_CODE_SIGN_FLAGS=#{("--keychain #{ci_keychain_path}").shellescape}"
end
build_app( build_app(
project: PROJECT_FILE, project: PROJECT_FILE,
scheme: SCHEME, scheme: SCHEME,
export_method: "app-store", export_method: "app-store",
codesigning_identity: "Apple Distribution", codesigning_identity: "Apple Distribution",
xcargs: [ xcargs: xcargs.join(" "),
"DEVELOPMENT_TEAM=#{TEAM_ID.shellescape}",
"CODE_SIGN_STYLE=Manual",
"CODE_SIGN_IDENTITY=Apple\\ Distribution",
"PROVISIONING_PROFILE_SPECIFIER=#{PROFILE_NAME.shellescape}"
].join(" "),
export_options: { export_options: {
signingStyle: "manual", signingStyle: "manual",
teamID: TEAM_ID, teamID: TEAM_ID,
@@ -149,5 +199,7 @@ platform :ios do
api_key: api_key, api_key: api_key,
skip_waiting_for_build_processing: true skip_waiting_for_build_processing: true
) )
ensure
cleanup_ci_signing
end end
end end
+4 -45
View File
@@ -28,45 +28,6 @@ import type { ChatMessage } from "../types.js";
const INTERNAL_CORRECTION = const INTERNAL_CORRECTION =
"Internal correction: the previous assistant message claimed it would run a tool, but no tool call was made. If the task needs an available tool, call it now. Otherwise provide the final answer directly without saying you will run a tool."; "Internal correction: the previous assistant message claimed it would run a tool, but no tool call was made. If the task needs an available tool, call it now. Otherwise provide the final answer directly without saying you will run a tool.";
const DEFAULT_ANTHROPIC_MAX_TOKENS = 128_000;
const MODEL_MAX_TOKENS_CACHE_MS = 24 * 60 * 60 * 1000;
const modelMaxTokensCache = new Map<string, { maxTokens: number; expiresAt: number }>();
function readMaxTokens(value: unknown) {
return Number.isSafeInteger(value) && (value as number) > 0 ? (value as number) : undefined;
}
function getModelInfoMaxTokens(modelInfo: any) {
return readMaxTokens(modelInfo?.max_tokens) ?? readMaxTokens(modelInfo?.maxTokens);
}
async function getMessagesMaxTokens(params: ToolAwareCompletionParams) {
if (params.maxTokens) return params.maxTokens;
const cached = modelMaxTokensCache.get(params.model);
if (cached && cached.expiresAt > Date.now()) return cached.maxTokens;
try {
const retrieve = params.client?.models?.retrieve;
if (typeof retrieve === "function") {
const modelInfo = await retrieve.call(params.client.models, params.model);
const maxTokens = getModelInfoMaxTokens(modelInfo);
if (maxTokens) {
modelMaxTokensCache.set(params.model, {
maxTokens,
expiresAt: Date.now() + MODEL_MAX_TOKENS_CACHE_MS,
});
return maxTokens;
}
}
} catch {
// Fall back to the documented max for Claude Opus 4.8 and related high-output models.
}
return DEFAULT_ANTHROPIC_MAX_TOKENS;
}
function toTools(tools: any[]) { function toTools(tools: any[]) {
return tools return tools
.map((tool) => { .map((tool) => {
@@ -199,12 +160,11 @@ function mergeUsage(acc: Required<ToolAwareUsage>, usage: any) {
export async function completeWithMessagesApi(params: ToolAwareCompletionParams): Promise<ToolAwareCompletionResult> { export async function completeWithMessagesApi(params: ToolAwareCompletionParams): Promise<ToolAwareCompletionResult> {
const enabledTools = getEnabledChatTools(params); const enabledTools = getEnabledChatTools(params);
const maxTokens = await getMessagesMaxTokens(params);
if (!enabledTools.length) { if (!enabledTools.length) {
const response = await params.client.messages.create({ const response = await params.client.messages.create({
model: params.model, model: params.model,
system: buildTopLevelSystemPrompt(params.messages, params.userLocation), system: buildTopLevelSystemPrompt(params.messages, params.userLocation),
max_tokens: maxTokens, max_tokens: params.maxTokens ?? 1024,
temperature: params.temperature, temperature: params.temperature,
messages: buildBaseMessages(params), messages: buildBaseMessages(params),
} as any); } as any);
@@ -232,7 +192,7 @@ export async function completeWithMessagesApi(params: ToolAwareCompletionParams)
const response = await params.client.messages.create({ const response = await params.client.messages.create({
model: params.model, model: params.model,
system: buildTopLevelSystemPrompt(params.messages, params.userLocation, buildChatToolSystemPrompt(params)), system: buildTopLevelSystemPrompt(params.messages, params.userLocation, buildChatToolSystemPrompt(params)),
max_tokens: maxTokens, max_tokens: params.maxTokens ?? 1024,
temperature: params.temperature, temperature: params.temperature,
messages: conversation, messages: conversation,
tools: toTools(enabledTools), tools: toTools(enabledTools),
@@ -288,7 +248,6 @@ export async function completeWithMessagesApi(params: ToolAwareCompletionParams)
export async function* streamWithMessagesApi(params: ToolAwareCompletionParams): AsyncGenerator<ToolAwareStreamingEvent> { export async function* streamWithMessagesApi(params: ToolAwareCompletionParams): AsyncGenerator<ToolAwareStreamingEvent> {
const enabledTools = getEnabledChatTools(params); const enabledTools = getEnabledChatTools(params);
const maxTokens = await getMessagesMaxTokens(params);
if (!enabledTools.length) { if (!enabledTools.length) {
const rawResponses: unknown[] = []; const rawResponses: unknown[] = [];
const usageAcc: Required<ToolAwareUsage> = { inputTokens: 0, outputTokens: 0, totalTokens: 0 }; const usageAcc: Required<ToolAwareUsage> = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
@@ -300,7 +259,7 @@ export async function* streamWithMessagesApi(params: ToolAwareCompletionParams):
const stream = await params.client.messages.create({ const stream = await params.client.messages.create({
model: params.model, model: params.model,
system: buildTopLevelSystemPrompt(params.messages, params.userLocation), system: buildTopLevelSystemPrompt(params.messages, params.userLocation),
max_tokens: maxTokens, max_tokens: params.maxTokens ?? 1024,
temperature: params.temperature, temperature: params.temperature,
messages: buildBaseMessages(params), messages: buildBaseMessages(params),
stream: true, stream: true,
@@ -356,7 +315,7 @@ export async function* streamWithMessagesApi(params: ToolAwareCompletionParams):
const stream = await params.client.messages.create({ const stream = await params.client.messages.create({
model: params.model, model: params.model,
system: buildTopLevelSystemPrompt(params.messages, params.userLocation, buildChatToolSystemPrompt(params)), system: buildTopLevelSystemPrompt(params.messages, params.userLocation, buildChatToolSystemPrompt(params)),
max_tokens: maxTokens, max_tokens: params.maxTokens ?? 1024,
temperature: params.temperature, temperature: params.temperature,
messages: conversation, messages: conversation,
tools: toTools(enabledTools), tools: toTools(enabledTools),
-88
View File
@@ -140,94 +140,6 @@ test("plain Chat Completions stream does not send Sybil-managed tools", async ()
assert.equal(events.at(-1)?.type === "done" ? events.at(-1)?.result.text : null, "Hi"); assert.equal(events.at(-1)?.type === "done" ? events.at(-1)?.result.text : null, "Hi");
}); });
test("Messages API defaults max_tokens to the Anthropic model maximum", async () => {
let requestBody: any = null;
let retrievedModel: string | null = null;
const client = {
models: {
retrieve: async (model: string) => {
retrievedModel = model;
return { id: model, max_tokens: 128000 };
},
},
messages: {
create: async (body: any) => {
requestBody = body;
return {
content: [{ type: "text", text: "Done" }],
usage: { input_tokens: 1, output_tokens: 1 },
};
},
},
};
const result = await completeWithMessagesApi({
client: client as any,
model: "claude-max-default-test",
messages: [{ role: "user", content: "Say done" }],
});
assert.equal(retrievedModel, "claude-max-default-test");
assert.equal(requestBody?.max_tokens, 128000);
assert.equal(result.text, "Done");
});
test("Messages API preserves explicit maxTokens", async () => {
let requestBody: any = null;
let didRetrieveModel = false;
const client = {
models: {
retrieve: async () => {
didRetrieveModel = true;
return { max_tokens: 128000 };
},
},
messages: {
create: async (body: any) => {
requestBody = body;
return streamFrom([
{
type: "message_start",
message: {
usage: { input_tokens: 1, output_tokens: 0 },
},
},
{
type: "content_block_start",
index: 0,
content_block: { type: "text", text: "" },
},
{
type: "content_block_delta",
index: 0,
delta: { type: "text_delta", text: "Done" },
},
{ type: "content_block_stop", index: 0 },
{
type: "message_delta",
delta: { stop_reason: "end_turn", stop_sequence: null },
usage: { output_tokens: 1 },
},
{ type: "message_stop" },
]);
},
},
};
const events = await collectEvents(
streamWithMessagesApi({
client: client as any,
model: "claude-explicit-max-test",
messages: [{ role: "user", content: "Say done" }],
maxTokens: 4096,
})
);
assert.equal(didRetrieveModel, false);
assert.equal(requestBody?.max_tokens, 4096);
assert.equal(events.at(-1)?.type === "done" ? events.at(-1)?.result.text : null, "Done");
});
test("fetch_url sends browser-like navigation headers", async () => { test("fetch_url sends browser-like navigation headers", async () => {
const originalFetch = globalThis.fetch; const originalFetch = globalThis.fetch;
const fetchCalls: Array<{ input: RequestInfo | URL; init?: RequestInit }> = []; const fetchCalls: Array<{ input: RequestInfo | URL; init?: RequestInit }> = [];
+1 -30
View File
@@ -286,14 +286,6 @@ textarea {
word-break: break-word; word-break: break-word;
} }
.md-content > :first-child {
margin-top: 0;
}
.md-content > :last-child {
margin-bottom: 0;
}
.md-table-scroll { .md-table-scroll {
max-width: 100%; max-width: 100%;
margin: 0.35rem 0 1rem; margin: 0.35rem 0 1rem;
@@ -392,8 +384,7 @@ textarea {
.md-content ul, .md-content ul,
.md-content ol { .md-content ol {
margin-top: 0.85rem; margin-top: 0.65rem;
margin-bottom: 0.85rem;
margin-left: 0; margin-left: 0;
padding-left: 0; padding-left: 0;
list-style: none; list-style: none;
@@ -405,26 +396,6 @@ textarea {
padding-left: 1.35rem; padding-left: 1.35rem;
} }
.md-content ul > li {
position: relative;
padding-left: 1.1rem;
}
.md-content ul > li::before {
content: "";
position: absolute;
left: 0;
top: 0.76em;
width: 0.36rem;
height: 0.36rem;
border-radius: 9999px;
background: hsl(188 86% 62%);
box-shadow:
0 0 0 2px hsl(188 86% 62% / 0.12),
0 0 10px hsl(188 86% 62% / 0.42);
transform: translateY(-50%);
}
.md-content li + li { .md-content li + li {
margin-top: 0.3rem; margin-top: 0.3rem;
} }