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/cli-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ jobs:
- name: Build CLI (Unix)
if: runner.os != 'Windows'
shell: bash
env:
# Keep the CLI self-updater and SSH installer on the same embedded
# minisign trust root used by the Desktop updater.
BITFUN_RELEASE_PUBKEY: ${{ secrets.TAURI_UPDATER_PUBKEY }}
run: |
set -euo pipefail
cargo build --release \
Expand Down Expand Up @@ -168,6 +172,19 @@ jobs:
run: |
./scripts/cli/package-windows.ps1 -Version $env:VERSION -Target $env:TARGET

- name: Sign CLI archive and checksum (macOS)
if: runner.os == 'macOS'
shell: bash
env:
BITFUN_SIGNING_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
BITFUN_SIGNING_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
BITFUN_SIGNING_PUBKEY: ${{ secrets.TAURI_UPDATER_PUBKEY }}
run: |
set -euo pipefail
bash scripts/sign-release-assets.sh \
"${{ steps.stage.outputs.archive }}" \
"${{ steps.stage.outputs.checksum }}"

- name: Upload artifact
uses: actions/upload-artifact@v6
with:
Expand All @@ -176,6 +193,8 @@ jobs:
path: |
${{ steps.stage.outputs.archive || steps.stage-windows.outputs.archive }}
${{ steps.stage.outputs.checksum || steps.stage-windows.outputs.checksum }}
bitfun-cli-*.tar.gz.sig
bitfun-cli-*.tar.gz.sha256.sig

# ── Aggregate and upload to GitHub Release ─────────────────────────
upload-release-assets:
Expand Down Expand Up @@ -227,6 +246,8 @@ jobs:
files: |
cli-release-assets/bitfun-cli-*.tar.gz
cli-release-assets/bitfun-cli-*.tar.gz.sha256
cli-release-assets/bitfun-cli-*.tar.gz.sig
cli-release-assets/bitfun-cli-*.tar.gz.sha256.sig
cli-release-assets/bitfun-cli-*.zip
cli-release-assets/bitfun-cli-*.zip.sha256
cli-release-assets/SHA256SUMS
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/desktop-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,7 @@ jobs:
linux-release-assets/bitfun-relay-server-*.tar.gz
linux-release-assets/bitfun-relay-server-*.tar.gz.sha256
linux-release-assets/*.tar.gz.sig
linux-release-assets/*.tar.gz.sha256.sig
linux-release-assets/linux-binaries.json
fail_on_unmatched_files: true

Expand All @@ -393,6 +394,10 @@ jobs:
"https://github.com/GCWing/BitFun/releases/download/${{ needs.prepare.outputs.release_tag }}/linux-binaries.json" \
-o linux-binaries.published.json
test "$(jq -r '.version' linux-binaries.published.json)" = "${{ needs.prepare.outputs.version }}"
while IFS= read -r cli_url; do
curl -fsSL --retry 5 --retry-delay 3 "${cli_url}.sig" -o /dev/null
curl -fsSL --retry 5 --retry-delay 3 "${cli_url}.sha256.sig" -o /dev/null
done < <(jq -r '.platforms[].cli.url' linux-binaries.published.json)

# Nudge the openbitfun.com mirror to sync now instead of on its next
# 10-minute cron tick. Until the mirror has these bytes, CN clients have
Expand Down
58 changes: 58 additions & 0 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,32 @@ jobs:
- name: Build desktop app
run: ${{ matrix.platform.build_command }}

- name: Package macOS CLI for SSH dispatch
if: runner.os == 'macOS'
id: macos-cli
shell: bash
env:
NIGHTLY_VERSION: ${{ needs.check-changes.outputs.nightly_version }}
TARGET: ${{ matrix.platform.target }}
run: |
set -euo pipefail
ASSET_VERSION="${NIGHTLY_VERSION%%+*}"
cargo build --release --target "$TARGET" -p bitfun-cli
bash scripts/cli/package-unix.sh "$ASSET_VERSION" "$TARGET"

- name: Sign macOS CLI archive and checksum
if: runner.os == 'macOS'
shell: bash
env:
BITFUN_SIGNING_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
BITFUN_SIGNING_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
BITFUN_SIGNING_PUBKEY: ${{ secrets.TAURI_UPDATER_PUBKEY }}
run: |
set -euo pipefail
bash scripts/sign-release-assets.sh \
"${{ steps.macos-cli.outputs.archive }}" \
"${{ steps.macos-cli.outputs.checksum }}"

- name: Verify AppImage fcitx5 GTK module
if: runner.os == 'Linux'
shell: bash
Expand All @@ -224,6 +250,10 @@ jobs:
target/release/bundle
src/apps/desktop/target/release/bundle
BitFun-Installer/src-tauri/target/release/bitfun-installer.exe
bitfun-cli-*-apple-darwin.tar.gz
bitfun-cli-*-apple-darwin.tar.gz.sha256
bitfun-cli-*-apple-darwin.tar.gz.sig
bitfun-cli-*-apple-darwin.tar.gz.sha256.sig

linux-binaries:
name: Linux CLI and Relay Server
Expand Down Expand Up @@ -343,9 +373,37 @@ jobs:
release-assets/**/*bitfun-installer.exe
release-assets/**/*.sig
release-assets/minisign.pub
release-assets/**/bitfun-cli-*-apple-darwin.tar.gz
release-assets/**/bitfun-cli-*-apple-darwin.tar.gz.sha256
linux-release-assets/bitfun-cli-*.tar.gz
linux-release-assets/bitfun-cli-*.tar.gz.sha256
linux-release-assets/bitfun-relay-server-*.tar.gz
linux-release-assets/bitfun-relay-server-*.tar.gz.sha256
linux-release-assets/*.tar.gz.sig
linux-release-assets/*.tar.gz.sha256.sig
linux-release-assets/linux-binaries.json

- name: Verify published Linux CLI signatures
shell: bash
run: |
set -euo pipefail
while IFS= read -r cli_url; do
curl -fsSL --retry 5 --retry-delay 3 "${cli_url}.sig" -o /dev/null
curl -fsSL --retry 5 --retry-delay 3 "${cli_url}.sha256.sig" -o /dev/null
done < <(jq -r '.platforms[].cli.url' linux-release-assets/linux-binaries.json)

- name: Verify published macOS CLI assets
shell: bash
env:
NIGHTLY_VERSION: ${{ needs.check-changes.outputs.nightly_version }}
run: |
set -euo pipefail
ASSET_VERSION="${NIGHTLY_VERSION%%+*}"
RELEASE_ROOT="https://github.com/GCWing/BitFun/releases/download/${{ env.NIGHTLY_TAG }}"
for target in aarch64-apple-darwin x86_64-apple-darwin; do
archive="${RELEASE_ROOT}/bitfun-cli-${ASSET_VERSION}-${target}.tar.gz"
curl -fsSL --retry 5 --retry-delay 3 "$archive" -o /dev/null
curl -fsSL --retry 5 --retry-delay 3 "${archive}.sha256" -o /dev/null
curl -fsSL --retry 5 --retry-delay 3 "${archive}.sig" -o /dev/null
curl -fsSL --retry 5 --retry-delay 3 "${archive}.sha256.sig" -o /dev/null
done
14 changes: 14 additions & 0 deletions scripts/relay/package-contract.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ test('formal and nightly releases gate publication on Linux binaries', () => {
assert.match(workflow, /needs:\s*\[[^\]]*linux-binaries[^\]]*\]/);
assert.match(workflow, /bitfun-relay-server-\*\.tar\.gz/);
assert.match(workflow, /bitfun-cli-\*\.tar\.gz/);
assert.match(workflow, /linux-release-assets\/\*\.tar\.gz\.sig/);
assert.match(workflow, /linux-release-assets\/\*\.tar\.gz\.sha256\.sig/);
assert.match(workflow, /\$\{cli_url\}\.sha256\.sig/);
assert.match(workflow, /linux-binaries\.json/);
}

Expand Down Expand Up @@ -65,3 +68,14 @@ test('release asset names carry no SemVer build metadata', () => {
assert.doesNotMatch(reusable, /package-unix\.sh "\$VERSION"/);
assert.match(nightly, /--version "\$\{NIGHTLY_VERSION%%\+\*\}"/);
});

test('nightly publishes signed macOS CLI archives for SSH dispatch', () => {
const nightly = read('.github/workflows/nightly.yml');

assert.match(nightly, /Package macOS CLI for SSH dispatch/);
assert.match(nightly, /scripts\/cli\/package-unix\.sh "\$ASSET_VERSION" "\$TARGET"/);
assert.match(nightly, /steps\.macos-cli\.outputs\.archive/);
assert.match(nightly, /bitfun-cli-\*-apple-darwin\.tar\.gz\.sha256\.sig/);
assert.match(nightly, /for target in aarch64-apple-darwin x86_64-apple-darwin/);
assert.match(nightly, /\$\{archive\}\.sha256\.sig/);
});
61 changes: 5 additions & 56 deletions src/apps/cli/src/agent/runtime_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@ use bitfun_agent_runtime::sdk::{
AgentTurnCancellationRequest, AgentTurnSettlementRequest, AgentUserAnswersRequest,
PermissionReply, PermissionRequest, PermissionRequestEventReceiver, PortError, PortErrorKind,
RuntimeError, SessionTranscript, SessionTranscriptRequest, SessionUsageReport,
AUTO_APPROVE_ASK_CONTEXT_KEY,
};
use bitfun_agent_runtime::user_questions::USER_INPUT_AVAILABLE_CONTEXT_KEY;
use bitfun_agent_runtime_ipc::{
RuntimeIpcClient, RuntimeIpcClientError, RuntimeIpcClientEvent, RuntimeIpcErrorCode,
RuntimeIpcEvent, RuntimeIpcOperation, RuntimeIpcOperationResult,
Expand All @@ -33,7 +31,7 @@ use bitfun_runtime_ports::{
};

use crate::actions::SHARED_TUI_EMBEDDED_HANDOFF;
use crate::runtime::approval::CliApprovalPolicy;
use crate::runtime::approval::{approval_metadata, CliApprovalPolicy};
use crate::runtime::CliRuntimeContext;

fn shared_restore_error(error: RuntimeIpcClientError) -> anyhow::Error {
Expand Down Expand Up @@ -64,33 +62,6 @@ fn validated_session_summary(
})
}

fn cli_approval_metadata(
approval_policy: CliApprovalPolicy,
) -> serde_json::Map<String, serde_json::Value> {
let mut metadata = serde_json::Map::new();
if matches!(
approval_policy,
CliApprovalPolicy::Reject | CliApprovalPolicy::Auto
) {
metadata.insert(
USER_INPUT_AVAILABLE_CONTEXT_KEY.to_string(),
serde_json::Value::Bool(false),
);
}
let auto_approve_ask = match approval_policy {
CliApprovalPolicy::Ask => None,
CliApprovalPolicy::DisableAuto | CliApprovalPolicy::Reject => Some(false),
CliApprovalPolicy::Auto => Some(true),
};
if let Some(auto_approve_ask) = auto_approve_ask {
metadata.insert(
AUTO_APPROVE_ASK_CONTEXT_KEY.to_string(),
serde_json::Value::Bool(auto_approve_ask),
);
}
metadata
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct SessionModeMigrationNotice {
pub(crate) previous_mode_id: String,
Expand Down Expand Up @@ -815,7 +786,7 @@ impl CliAgentRuntimeClient {
}

// Start the dialog turn; events arrive through the shared broadcast source.
let metadata = cli_approval_metadata(self.approval_policy());
let metadata = approval_metadata(self.approval_policy());
let request = AgentDialogTurnRequest {
session_id: session_id.clone(),
message: message.clone(),
Expand Down Expand Up @@ -1197,17 +1168,13 @@ mod tests {

use bitfun_agent_runtime::sdk::{
PermissionDelegationContext, PermissionRequest, PermissionRequestEvent,
PermissionRequestSource, PermissionRequestSourceKind, AUTO_APPROVE_ASK_CONTEXT_KEY,
PermissionRequestSource, PermissionRequestSourceKind,
};
use bitfun_agent_runtime::user_questions::USER_INPUT_AVAILABLE_CONTEXT_KEY;
use bitfun_agent_runtime_ipc::{RuntimeIpcClientError, RuntimeIpcError, RuntimeIpcErrorCode};

use crate::runtime::approval::CliApprovalPolicy;

use super::{
cli_approval_metadata, project_routed_permission_event, session_mode_migration_notice,
shared_disconnect_message, shared_restore_error, validated_session_summary,
CliWorkspacePaths,
project_routed_permission_event, session_mode_migration_notice, shared_disconnect_message,
shared_restore_error, validated_session_summary, CliWorkspacePaths,
};
use bitfun_agent_runtime_ipc::RuntimeIpcStreamInvalidationReason;

Expand Down Expand Up @@ -1274,24 +1241,6 @@ mod tests {
.is_none());
}

#[test]
fn cli_approval_metadata_keeps_auto_invocation_scoped() {
let auto = cli_approval_metadata(CliApprovalPolicy::Auto);
assert_eq!(auto[AUTO_APPROVE_ASK_CONTEXT_KEY], true);
assert_eq!(auto[USER_INPUT_AVAILABLE_CONTEXT_KEY], false);

let reject = cli_approval_metadata(CliApprovalPolicy::Reject);
assert_eq!(reject[AUTO_APPROVE_ASK_CONTEXT_KEY], false);

let ask = cli_approval_metadata(CliApprovalPolicy::Ask);
assert!(!ask.contains_key(AUTO_APPROVE_ASK_CONTEXT_KEY));
assert!(!ask.contains_key(USER_INPUT_AVAILABLE_CONTEXT_KEY));

let disabled = cli_approval_metadata(CliApprovalPolicy::DisableAuto);
assert_eq!(disabled[AUTO_APPROVE_ASK_CONTEXT_KEY], false);
assert!(!disabled.contains_key(USER_INPUT_AVAILABLE_CONTEXT_KEY));
}

#[test]
fn model_updates_use_the_runtime_sdk_without_the_core_compatibility_facade() {
let source = include_str!("runtime_client.rs").replace("\r\n", "\n");
Expand Down
Loading