mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 14:53:34 +00:00
ci: Added CI/CD and release pipelines (#45)
This commit is contained in:
@@ -0,0 +1,21 @@
|
|||||||
|
name: Code Style
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
codestyle:
|
||||||
|
name: Code Style (fmt + clippy)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Install Rust
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
profile: minimal
|
||||||
|
components: rustfmt, clippy
|
||||||
|
- name: Check formatting
|
||||||
|
run: |
|
||||||
|
cargo fmt --all -- --check
|
||||||
|
- name: Check lints (cargo clippy)
|
||||||
|
run: cargo clippy -- -D warnings
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
name: Release-plz
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release-plz:
|
||||||
|
name: Release-plz
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Install Rust toolchain
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
- name: Install packages (Linux)
|
||||||
|
if: runner.os == 'Linux'
|
||||||
|
run: sudo apt-get update && sudo apt-get install --assume-yes libudev-dev
|
||||||
|
- name: Run release-plz
|
||||||
|
uses: MarcoIeni/[email protected]
|
||||||
|
env:
|
||||||
|
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||||
@@ -0,0 +1,324 @@
|
|||||||
|
# This file was autogenerated by dist: https://opensource.axo.dev/cargo-dist/
|
||||||
|
#
|
||||||
|
# Copyright 2022-2024, axodotdev
|
||||||
|
# SPDX-License-Identifier: MIT or Apache-2.0
|
||||||
|
#
|
||||||
|
# CI that:
|
||||||
|
#
|
||||||
|
# * checks for a Git Tag that looks like a release
|
||||||
|
# * builds artifacts with dist (archives, installers, hashes)
|
||||||
|
# * uploads those artifacts to temporary workflow zip
|
||||||
|
# * on success, uploads the artifacts to a GitHub Release
|
||||||
|
#
|
||||||
|
# Note that the GitHub Release will be created with a generated
|
||||||
|
# title/body based on your changelogs.
|
||||||
|
|
||||||
|
name: Release
|
||||||
|
permissions:
|
||||||
|
"contents": "write"
|
||||||
|
|
||||||
|
# This task will run whenever you push a git tag that looks like a version
|
||||||
|
# like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc.
|
||||||
|
# Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where
|
||||||
|
# PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION
|
||||||
|
# must be a Cargo-style SemVer Version (must have at least major.minor.patch).
|
||||||
|
#
|
||||||
|
# If PACKAGE_NAME is specified, then the announcement will be for that
|
||||||
|
# package (erroring out if it doesn't have the given version or isn't dist-able).
|
||||||
|
#
|
||||||
|
# If PACKAGE_NAME isn't specified, then the announcement will be for all
|
||||||
|
# (dist-able) packages in the workspace with that version (this mode is
|
||||||
|
# intended for workspaces with only one dist-able package, or with all dist-able
|
||||||
|
# packages versioned/released in lockstep).
|
||||||
|
#
|
||||||
|
# If you push multiple tags at once, separate instances of this workflow will
|
||||||
|
# spin up, creating an independent announcement for each one. However, GitHub
|
||||||
|
# will hard limit this to 3 tags per commit, as it will assume more tags is a
|
||||||
|
# mistake.
|
||||||
|
#
|
||||||
|
# If there's a prerelease-style suffix to the version, then the release(s)
|
||||||
|
# will be marked as a prerelease.
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '**[0-9]+.[0-9]+.[0-9]+*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# Run 'dist plan' (or host) to determine what tasks we need to do
|
||||||
|
plan:
|
||||||
|
runs-on: "ubuntu-22.04"
|
||||||
|
outputs:
|
||||||
|
val: ${{ steps.plan.outputs.manifest }}
|
||||||
|
tag: ${{ !github.event.pull_request && github.ref_name || '' }}
|
||||||
|
tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }}
|
||||||
|
publishing: ${{ !github.event.pull_request }}
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
- name: Install dist
|
||||||
|
# we specify bash to get pipefail; it guards against the `curl` command
|
||||||
|
# failing. otherwise `sh` won't catch that `curl` returned non-0
|
||||||
|
shell: bash
|
||||||
|
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.30.3/cargo-dist-installer.sh | sh"
|
||||||
|
- name: Cache dist
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: cargo-dist-cache
|
||||||
|
path: ~/.cargo/bin/dist
|
||||||
|
# sure would be cool if github gave us proper conditionals...
|
||||||
|
# so here's a doubly-nested ternary-via-truthiness to try to provide the best possible
|
||||||
|
# functionality based on whether this is a pull_request, and whether it's from a fork.
|
||||||
|
# (PRs run on the *source* but secrets are usually on the *target* -- that's *good*
|
||||||
|
# but also really annoying to build CI around when it needs secrets to work right.)
|
||||||
|
- id: plan
|
||||||
|
run: |
|
||||||
|
dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json
|
||||||
|
echo "dist ran successfully"
|
||||||
|
cat plan-dist-manifest.json
|
||||||
|
echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT"
|
||||||
|
- name: "Upload dist-manifest.json"
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: artifacts-plan-dist-manifest
|
||||||
|
path: plan-dist-manifest.json
|
||||||
|
|
||||||
|
# Build and packages all the platform-specific things
|
||||||
|
build-local-artifacts:
|
||||||
|
name: build-local-artifacts (${{ join(matrix.targets, ', ') }})
|
||||||
|
# Let the initial task tell us to not run (currently very blunt)
|
||||||
|
needs:
|
||||||
|
- plan
|
||||||
|
if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
# Target platforms/runners are computed by dist in create-release.
|
||||||
|
# Each member of the matrix has the following arguments:
|
||||||
|
#
|
||||||
|
# - runner: the github runner
|
||||||
|
# - dist-args: cli flags to pass to dist
|
||||||
|
# - install-dist: expression to run to install dist on the runner
|
||||||
|
#
|
||||||
|
# Typically there will be:
|
||||||
|
# - 1 "global" task that builds universal installers
|
||||||
|
# - N "local" tasks that build each platform's binaries and platform-specific installers
|
||||||
|
matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }}
|
||||||
|
runs-on: ${{ matrix.runner }}
|
||||||
|
container: ${{ matrix.container && matrix.container.image || null }}
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json
|
||||||
|
steps:
|
||||||
|
- name: enable windows longpaths
|
||||||
|
run: |
|
||||||
|
git config --global core.longpaths true
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
- name: Install Rust non-interactively if not already installed
|
||||||
|
if: ${{ matrix.container }}
|
||||||
|
run: |
|
||||||
|
if ! command -v cargo > /dev/null 2>&1; then
|
||||||
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||||
|
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||||
|
fi
|
||||||
|
- uses: swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
key: ${{ join(matrix.targets, '-') }}
|
||||||
|
cache-provider: ${{ matrix.cache_provider }}
|
||||||
|
- name: Install dist
|
||||||
|
run: ${{ matrix.install_dist.run }}
|
||||||
|
# Get the dist-manifest
|
||||||
|
- name: Fetch local artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
pattern: artifacts-*
|
||||||
|
path: target/distrib/
|
||||||
|
merge-multiple: true
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
${{ matrix.packages_install }}
|
||||||
|
- name: Build artifacts
|
||||||
|
run: |
|
||||||
|
# Actually do builds and make zips and whatnot
|
||||||
|
dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
|
||||||
|
echo "dist ran successfully"
|
||||||
|
- id: cargo-dist
|
||||||
|
name: Post-build
|
||||||
|
# We force bash here just because github makes it really hard to get values up
|
||||||
|
# to "real" actions without writing to env-vars, and writing to env-vars has
|
||||||
|
# inconsistent syntax between shell and powershell.
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
# Parse out what we just built and upload it to scratch storage
|
||||||
|
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
|
||||||
|
dist print-upload-files-from-manifest --manifest dist-manifest.json >> "$GITHUB_OUTPUT"
|
||||||
|
echo "EOF" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
|
||||||
|
- name: "Upload artifacts"
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: artifacts-build-local-${{ join(matrix.targets, '_') }}
|
||||||
|
path: |
|
||||||
|
${{ steps.cargo-dist.outputs.paths }}
|
||||||
|
${{ env.BUILD_MANIFEST_NAME }}
|
||||||
|
|
||||||
|
# Build and package all the platform-agnostic(ish) things
|
||||||
|
build-global-artifacts:
|
||||||
|
needs:
|
||||||
|
- plan
|
||||||
|
- build-local-artifacts
|
||||||
|
runs-on: "ubuntu-22.04"
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
- name: Install cached dist
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: cargo-dist-cache
|
||||||
|
path: ~/.cargo/bin/
|
||||||
|
- run: chmod +x ~/.cargo/bin/dist
|
||||||
|
# Get all the local artifacts for the global tasks to use (for e.g. checksums)
|
||||||
|
- name: Fetch local artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
pattern: artifacts-*
|
||||||
|
path: target/distrib/
|
||||||
|
merge-multiple: true
|
||||||
|
- id: cargo-dist
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json
|
||||||
|
echo "dist ran successfully"
|
||||||
|
|
||||||
|
# Parse out what we just built and upload it to scratch storage
|
||||||
|
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
|
||||||
|
jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT"
|
||||||
|
echo "EOF" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
|
||||||
|
- name: "Upload artifacts"
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: artifacts-build-global
|
||||||
|
path: |
|
||||||
|
${{ steps.cargo-dist.outputs.paths }}
|
||||||
|
${{ env.BUILD_MANIFEST_NAME }}
|
||||||
|
# Determines if we should publish/announce
|
||||||
|
host:
|
||||||
|
needs:
|
||||||
|
- plan
|
||||||
|
- build-local-artifacts
|
||||||
|
- build-global-artifacts
|
||||||
|
# Only run if we're "publishing", and only if local and global didn't fail (skipped is fine)
|
||||||
|
if: ${{ always() && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }}
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
runs-on: "ubuntu-22.04"
|
||||||
|
outputs:
|
||||||
|
val: ${{ steps.host.outputs.manifest }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
- name: Install cached dist
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: cargo-dist-cache
|
||||||
|
path: ~/.cargo/bin/
|
||||||
|
- run: chmod +x ~/.cargo/bin/dist
|
||||||
|
# Fetch artifacts from scratch-storage
|
||||||
|
- name: Fetch artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
pattern: artifacts-*
|
||||||
|
path: target/distrib/
|
||||||
|
merge-multiple: true
|
||||||
|
- id: host
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json
|
||||||
|
echo "artifacts uploaded and released successfully"
|
||||||
|
cat dist-manifest.json
|
||||||
|
echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
|
||||||
|
- name: "Upload dist-manifest.json"
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
# Overwrite the previous copy
|
||||||
|
name: artifacts-dist-manifest
|
||||||
|
path: dist-manifest.json
|
||||||
|
# Create a GitHub Release while uploading all files to it
|
||||||
|
- name: "Download GitHub Artifacts"
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
pattern: artifacts-*
|
||||||
|
path: artifacts
|
||||||
|
merge-multiple: true
|
||||||
|
- name: Cleanup
|
||||||
|
run: |
|
||||||
|
# Remove the granular manifests
|
||||||
|
rm -f artifacts/*-dist-manifest.json
|
||||||
|
- name: Create GitHub Release
|
||||||
|
env:
|
||||||
|
PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}"
|
||||||
|
ANNOUNCEMENT_TITLE: "${{ fromJson(steps.host.outputs.manifest).announcement_title }}"
|
||||||
|
ANNOUNCEMENT_BODY: "${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}"
|
||||||
|
RELEASE_COMMIT: "${{ github.sha }}"
|
||||||
|
run: |
|
||||||
|
# Write and read notes from a file to avoid quoting breaking things
|
||||||
|
echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt
|
||||||
|
|
||||||
|
gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/*
|
||||||
|
|
||||||
|
publish-npm:
|
||||||
|
needs:
|
||||||
|
- plan
|
||||||
|
- host
|
||||||
|
runs-on: "ubuntu-22.04"
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
PLAN: ${{ needs.plan.outputs.val }}
|
||||||
|
if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }}
|
||||||
|
steps:
|
||||||
|
- name: Fetch npm packages
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
pattern: artifacts-*
|
||||||
|
path: npm/
|
||||||
|
merge-multiple: true
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20.x'
|
||||||
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
- run: |
|
||||||
|
for release in $(echo "$PLAN" | jq --compact-output '.releases[] | select([.artifacts[] | endswith("-npm-package.tar.gz")] | any)'); do
|
||||||
|
pkg=$(echo "$release" | jq '.artifacts[] | select(endswith("-npm-package.tar.gz"))' --raw-output)
|
||||||
|
npm publish --access public "./npm/${pkg}"
|
||||||
|
done
|
||||||
|
env:
|
||||||
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|
||||||
|
announce:
|
||||||
|
needs:
|
||||||
|
- plan
|
||||||
|
- host
|
||||||
|
- publish-npm
|
||||||
|
# use "always() && ..." to allow us to wait for all publish jobs while
|
||||||
|
# still allowing individual publish jobs to skip themselves (for prereleases).
|
||||||
|
# "host" however must run to completion, no skipping allowed!
|
||||||
|
if: ${{ always() && needs.host.result == 'success' && (needs.publish-npm.result == 'skipped' || needs.publish-npm.result == 'success') }}
|
||||||
|
runs-on: "ubuntu-22.04"
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
name: Run Tests
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
tests:
|
||||||
|
name: Run Tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Install Rust
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
profile: minimal
|
||||||
|
- name: Run Tests
|
||||||
|
run: cargo test --all-features -- --nocapture
|
||||||
+53
@@ -4,7 +4,16 @@ version = "0.1.0"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.85"
|
rust-version = "1.85"
|
||||||
description = "Secure personal AI assistant that protects your data and expands its capabilities on the fly"
|
description = "Secure personal AI assistant that protects your data and expands its capabilities on the fly"
|
||||||
|
authors = ["NEAR AI <[email protected]>"]
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
|
homepage = "https://github.com/nearai/ironclaw"
|
||||||
|
repository = "https://github.com/nearai/ironclaw"
|
||||||
|
|
||||||
|
[package.metadata.wix]
|
||||||
|
upgrade-guid = "D0156E61-BA37-451E-8AB9-1A2ECCCFA48F"
|
||||||
|
path-guid = "F90B6EA6-87F7-499B-BB19-CF55DE1EB339"
|
||||||
|
license = false
|
||||||
|
eula = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Async runtime
|
# Async runtime
|
||||||
@@ -127,3 +136,47 @@ tempfile = "3"
|
|||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
integration = []
|
integration = []
|
||||||
|
|
||||||
|
# The profile that 'cargo dist' will build with
|
||||||
|
[profile.dist]
|
||||||
|
inherits = "release"
|
||||||
|
lto = "thin"
|
||||||
|
|
||||||
|
# Config for 'dist'
|
||||||
|
[workspace.metadata.dist]
|
||||||
|
# The preferred dist version to use in CI (Cargo.toml SemVer syntax)
|
||||||
|
cargo-dist-version = "0.30.3"
|
||||||
|
allow-dirty = ["ci"]
|
||||||
|
# CI backends to support
|
||||||
|
ci = "github"
|
||||||
|
# The installers to generate for each app
|
||||||
|
installers = ["shell", "powershell", "npm", "msi"]
|
||||||
|
# Publish jobs to run in CI
|
||||||
|
publish-jobs = ["npm"]
|
||||||
|
# Target platforms to build apps for (Rust target-triple syntax)
|
||||||
|
targets = [
|
||||||
|
"aarch64-apple-darwin",
|
||||||
|
"aarch64-unknown-linux-gnu",
|
||||||
|
"aarch64-pc-windows-msvc",
|
||||||
|
"x86_64-apple-darwin",
|
||||||
|
"x86_64-unknown-linux-gnu",
|
||||||
|
"x86_64-pc-windows-msvc",
|
||||||
|
]
|
||||||
|
# The archive format to use for windows builds (defaults .zip)
|
||||||
|
windows-archive = ".tar.gz"
|
||||||
|
# The archive format to use for non-windows builds (defaults .tar.xz)
|
||||||
|
unix-archive = ".tar.gz"
|
||||||
|
# Which actions to run on pull requests
|
||||||
|
pr-run-mode = "upload"
|
||||||
|
# Path that installers should place binaries in
|
||||||
|
install-path = "CARGO_HOME"
|
||||||
|
# Whether to install an updater program
|
||||||
|
install-updater = false
|
||||||
|
|
||||||
|
[workspace.metadata.dist.github-custom-runners]
|
||||||
|
aarch64-unknown-linux-gnu = "ubuntu-24.04-arm"
|
||||||
|
x86_64-unknown-linux-gnu = "ubuntu-22.04"
|
||||||
|
x86_64-pc-windows-msvc = "windows-2022"
|
||||||
|
aarch64-pc-windows-msvc = "windows-2025"
|
||||||
|
x86_64-apple-darwin = "macos-15-intel"
|
||||||
|
aarch64-apple-darwin = "macos-14"
|
||||||
|
|||||||
@@ -80,25 +80,26 @@ fn main() {
|
|||||||
.map(|s| s.success())
|
.map(|s| s.success())
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
|
||||||
if !component_ok
|
if !component_ok {
|
||||||
{
|
|
||||||
// Fallback: copy raw module if wasm-tools unavailable
|
// Fallback: copy raw module if wasm-tools unavailable
|
||||||
if std::fs::copy(&raw_wasm, &wasm_out).is_err() {
|
if std::fs::copy(&raw_wasm, &wasm_out).is_err() {
|
||||||
eprintln!(
|
eprintln!("cargo:warning=wasm-tools not found. Run: cargo install wasm-tools");
|
||||||
"cargo:warning=wasm-tools not found. Run: cargo install wasm-tools"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Strip debug info (use temp file to avoid clobbering)
|
// Strip debug info (use temp file to avoid clobbering)
|
||||||
let stripped = wasm_out.with_extension("wasm.stripped");
|
let stripped = wasm_out.with_extension("wasm.stripped");
|
||||||
let strip_ok = Command::new("wasm-tools")
|
let strip_ok = Command::new("wasm-tools")
|
||||||
.args(["strip", wasm_out.to_str().unwrap(), "-o", stripped.to_str().unwrap()])
|
.args([
|
||||||
|
"strip",
|
||||||
|
wasm_out.to_str().unwrap(),
|
||||||
|
"-o",
|
||||||
|
stripped.to_str().unwrap(),
|
||||||
|
])
|
||||||
.current_dir(&root)
|
.current_dir(&root)
|
||||||
.status()
|
.status()
|
||||||
.map(|s| s.success())
|
.map(|s| s.success())
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
if strip_ok
|
if strip_ok {
|
||||||
{
|
|
||||||
let _ = std::fs::rename(&stripped, &wasm_out);
|
let _ = std::fs::rename(&stripped, &wasm_out);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,9 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
println!("=== Heartbeat Integration Test ===\n");
|
println!("=== Heartbeat Integration Test ===\n");
|
||||||
|
|
||||||
// 1. Load config
|
// 1. Load config
|
||||||
let config = Config::from_env().await.map_err(|e| anyhow::anyhow!("Config: {}", e))?;
|
let config = Config::from_env()
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow::anyhow!("Config: {}", e))?;
|
||||||
println!("[1/6] Config loaded");
|
println!("[1/6] Config loaded");
|
||||||
println!(" heartbeat.enabled = {}", config.heartbeat.enabled);
|
println!(" heartbeat.enabled = {}", config.heartbeat.enabled);
|
||||||
println!(
|
println!(
|
||||||
|
|||||||
@@ -43,12 +43,12 @@ use wasmtime_wasi::{ResourceTable, WasiCtx, WasiCtxBuilder, WasiView};
|
|||||||
use crate::channels::wasm::capabilities::ChannelCapabilities;
|
use crate::channels::wasm::capabilities::ChannelCapabilities;
|
||||||
use crate::channels::wasm::error::WasmChannelError;
|
use crate::channels::wasm::error::WasmChannelError;
|
||||||
use crate::channels::wasm::host::{ChannelEmitRateLimiter, ChannelHostState, EmittedMessage};
|
use crate::channels::wasm::host::{ChannelEmitRateLimiter, ChannelHostState, EmittedMessage};
|
||||||
use crate::pairing::PairingStore;
|
|
||||||
use crate::channels::wasm::router::RegisteredEndpoint;
|
use crate::channels::wasm::router::RegisteredEndpoint;
|
||||||
use crate::channels::wasm::runtime::{PreparedChannelModule, WasmChannelRuntime};
|
use crate::channels::wasm::runtime::{PreparedChannelModule, WasmChannelRuntime};
|
||||||
use crate::channels::wasm::schema::ChannelConfig;
|
use crate::channels::wasm::schema::ChannelConfig;
|
||||||
use crate::channels::{Channel, IncomingMessage, MessageStream, OutgoingResponse, StatusUpdate};
|
use crate::channels::{Channel, IncomingMessage, MessageStream, OutgoingResponse, StatusUpdate};
|
||||||
use crate::error::ChannelError;
|
use crate::error::ChannelError;
|
||||||
|
use crate::pairing::PairingStore;
|
||||||
use crate::safety::LeakDetector;
|
use crate::safety::LeakDetector;
|
||||||
use crate::tools::wasm::LogLevel;
|
use crate::tools::wasm::LogLevel;
|
||||||
use crate::tools::wasm::WasmResourceLimiter;
|
use crate::tools::wasm::WasmResourceLimiter;
|
||||||
@@ -1190,6 +1190,7 @@ impl WasmChannel {
|
|||||||
///
|
///
|
||||||
/// Static method for use by the background typing repeat task (which
|
/// Static method for use by the background typing repeat task (which
|
||||||
/// doesn't have access to `&self`).
|
/// doesn't have access to `&self`).
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
async fn execute_status(
|
async fn execute_status(
|
||||||
channel_name: &str,
|
channel_name: &str,
|
||||||
runtime: &Arc<WasmChannelRuntime>,
|
runtime: &Arc<WasmChannelRuntime>,
|
||||||
@@ -2073,12 +2074,12 @@ mod tests {
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::channels::Channel;
|
use crate::channels::Channel;
|
||||||
use crate::pairing::PairingStore;
|
|
||||||
use crate::channels::wasm::capabilities::ChannelCapabilities;
|
use crate::channels::wasm::capabilities::ChannelCapabilities;
|
||||||
use crate::channels::wasm::runtime::{
|
use crate::channels::wasm::runtime::{
|
||||||
PreparedChannelModule, WasmChannelRuntime, WasmChannelRuntimeConfig,
|
PreparedChannelModule, WasmChannelRuntime, WasmChannelRuntimeConfig,
|
||||||
};
|
};
|
||||||
use crate::channels::wasm::wrapper::{HttpResponse, WasmChannel};
|
use crate::channels::wasm::wrapper::{HttpResponse, WasmChannel};
|
||||||
|
use crate::pairing::PairingStore;
|
||||||
use crate::tools::wasm::ResourceLimits;
|
use crate::tools::wasm::ResourceLimits;
|
||||||
|
|
||||||
fn create_test_channel() -> WasmChannel {
|
fn create_test_channel() -> WasmChannel {
|
||||||
@@ -2525,8 +2526,13 @@ mod tests {
|
|||||||
);
|
);
|
||||||
creds.insert("OTHER_SECRET".to_string(), "s3cret".to_string());
|
creds.insert("OTHER_SECRET".to_string(), "s3cret".to_string());
|
||||||
|
|
||||||
let store =
|
let store = ChannelStoreData::new(
|
||||||
ChannelStoreData::new(1024 * 1024, "test", ChannelCapabilities::default(), creds);
|
1024 * 1024,
|
||||||
|
"test",
|
||||||
|
ChannelCapabilities::default(),
|
||||||
|
creds,
|
||||||
|
Arc::new(PairingStore::new()),
|
||||||
|
);
|
||||||
|
|
||||||
let error = "HTTP request failed: error sending request for url \
|
let error = "HTTP request failed: error sending request for url \
|
||||||
(https://api.telegram.org/bot8218490433:AAEZeUxwqZ5OO3mOCXv7fKvpdhDgsmBBNis/getUpdates)";
|
(https://api.telegram.org/bot8218490433:AAEZeUxwqZ5OO3mOCXv7fKvpdhDgsmBBNis/getUpdates)";
|
||||||
@@ -2556,6 +2562,7 @@ mod tests {
|
|||||||
"test",
|
"test",
|
||||||
ChannelCapabilities::default(),
|
ChannelCapabilities::default(),
|
||||||
std::collections::HashMap::new(),
|
std::collections::HashMap::new(),
|
||||||
|
Arc::new(PairingStore::new()),
|
||||||
);
|
);
|
||||||
|
|
||||||
let input = "some error message";
|
let input = "some error message";
|
||||||
@@ -2569,8 +2576,13 @@ mod tests {
|
|||||||
let mut creds = std::collections::HashMap::new();
|
let mut creds = std::collections::HashMap::new();
|
||||||
creds.insert("EMPTY_TOKEN".to_string(), String::new());
|
creds.insert("EMPTY_TOKEN".to_string(), String::new());
|
||||||
|
|
||||||
let store =
|
let store = ChannelStoreData::new(
|
||||||
ChannelStoreData::new(1024 * 1024, "test", ChannelCapabilities::default(), creds);
|
1024 * 1024,
|
||||||
|
"test",
|
||||||
|
ChannelCapabilities::default(),
|
||||||
|
creds,
|
||||||
|
Arc::new(PairingStore::new()),
|
||||||
|
);
|
||||||
|
|
||||||
let input = "should not match anything";
|
let input = "should not match anything";
|
||||||
assert_eq!(store.redact_credentials(input), input);
|
assert_eq!(store.redact_credentials(input), input);
|
||||||
|
|||||||
+3
-1
@@ -71,7 +71,9 @@ pub async fn run_config_command(cmd: ConfigCommand) -> anyhow::Result<()> {
|
|||||||
|
|
||||||
/// Bootstrap a DB connection for config commands.
|
/// Bootstrap a DB connection for config commands.
|
||||||
async fn connect_store() -> anyhow::Result<crate::history::Store> {
|
async fn connect_store() -> anyhow::Result<crate::history::Store> {
|
||||||
let config = crate::config::Config::from_env().await.map_err(|e| anyhow::anyhow!("{}", e))?;
|
let config = crate::config::Config::from_env()
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow::anyhow!("{}", e))?;
|
||||||
let store = crate::history::Store::new(&config.database).await?;
|
let store = crate::history::Store::new(&config.database).await?;
|
||||||
store.run_migrations().await?;
|
store.run_migrations().await?;
|
||||||
Ok(store)
|
Ok(store)
|
||||||
|
|||||||
+9
-5
@@ -52,7 +52,10 @@ fn run_list(store: &PairingStore, channel: &str, json: bool) -> Result<(), Strin
|
|||||||
let requests = store.list_pending(channel).map_err(|e| e.to_string())?;
|
let requests = store.list_pending(channel).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
if json {
|
if json {
|
||||||
println!("{}", serde_json::to_string_pretty(&requests).map_err(|e| e.to_string())?);
|
println!(
|
||||||
|
"{}",
|
||||||
|
serde_json::to_string_pretty(&requests).map_err(|e| e.to_string())?
|
||||||
|
);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,9 +72,7 @@ fn run_list(store: &PairingStore, channel: &str, json: bool) -> Result<(), Strin
|
|||||||
.and_then(|m| m.as_object())
|
.and_then(|m| m.as_object())
|
||||||
.map(|o| {
|
.map(|o| {
|
||||||
o.iter()
|
o.iter()
|
||||||
.filter_map(|(k, v)| {
|
.filter_map(|(k, v)| v.as_str().map(|s| format!("{}={}", k, s)))
|
||||||
v.as_str().map(|s| format!("{}={}", k, s))
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(", ")
|
.join(", ")
|
||||||
})
|
})
|
||||||
@@ -88,7 +89,10 @@ fn run_approve(store: &PairingStore, channel: &str, code: &str) -> Result<(), St
|
|||||||
println!("Approved {} sender {}.", channel, entry.id);
|
println!("Approved {} sender {}.", channel, entry.id);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Ok(None) => Err(format!("No pending pairing request found for code: {}", code)),
|
Ok(None) => Err(format!(
|
||||||
|
"No pending pairing request found for code: {}",
|
||||||
|
code
|
||||||
|
)),
|
||||||
Err(crate::pairing::PairingStoreError::ApproveRateLimited) => Err(
|
Err(crate::pairing::PairingStoreError::ApproveRateLimited) => Err(
|
||||||
"Too many failed approve attempts. Wait a few minutes before trying again.".to_string(),
|
"Too many failed approve attempts. Wait a few minutes before trying again.".to_string(),
|
||||||
),
|
),
|
||||||
|
|||||||
+1
-1
@@ -43,7 +43,6 @@ pub mod bootstrap;
|
|||||||
pub mod channels;
|
pub mod channels;
|
||||||
pub mod cli;
|
pub mod cli;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod pairing;
|
|
||||||
pub mod context;
|
pub mod context;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod estimation;
|
pub mod estimation;
|
||||||
@@ -52,6 +51,7 @@ pub mod extensions;
|
|||||||
pub mod history;
|
pub mod history;
|
||||||
pub mod llm;
|
pub mod llm;
|
||||||
pub mod orchestrator;
|
pub mod orchestrator;
|
||||||
|
pub mod pairing;
|
||||||
pub mod safety;
|
pub mod safety;
|
||||||
pub mod sandbox;
|
pub mod sandbox;
|
||||||
pub mod secrets;
|
pub mod secrets;
|
||||||
|
|||||||
+4
-2
@@ -7,7 +7,6 @@ use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitEx
|
|||||||
|
|
||||||
use ironclaw::{
|
use ironclaw::{
|
||||||
agent::{Agent, AgentDeps, SessionManager},
|
agent::{Agent, AgentDeps, SessionManager},
|
||||||
pairing::PairingStore,
|
|
||||||
channels::{
|
channels::{
|
||||||
ChannelManager, GatewayChannel, HttpChannel, ReplChannel, WebhookServer,
|
ChannelManager, GatewayChannel, HttpChannel, ReplChannel, WebhookServer,
|
||||||
WebhookServerConfig,
|
WebhookServerConfig,
|
||||||
@@ -30,6 +29,7 @@ use ironclaw::{
|
|||||||
ContainerJobConfig, ContainerJobManager, OrchestratorApi, TokenStore,
|
ContainerJobConfig, ContainerJobManager, OrchestratorApi, TokenStore,
|
||||||
api::OrchestratorState,
|
api::OrchestratorState,
|
||||||
},
|
},
|
||||||
|
pairing::PairingStore,
|
||||||
safety::SafetyLayer,
|
safety::SafetyLayer,
|
||||||
secrets::{PostgresSecretsStore, SecretsCrypto, SecretsStore},
|
secrets::{PostgresSecretsStore, SecretsCrypto, SecretsStore},
|
||||||
setup::{SetupConfig, SetupWizard},
|
setup::{SetupConfig, SetupWizard},
|
||||||
@@ -86,7 +86,9 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
// Memory commands need database (and optionally embeddings)
|
// Memory commands need database (and optionally embeddings)
|
||||||
let _ = dotenvy::dotenv();
|
let _ = dotenvy::dotenv();
|
||||||
let config = Config::from_env().await.map_err(|e| anyhow::anyhow!("{}", e))?;
|
let config = Config::from_env()
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow::anyhow!("{}", e))?;
|
||||||
let store = ironclaw::history::Store::new(&config.database).await?;
|
let store = ironclaw::history::Store::new(&config.database).await?;
|
||||||
store.run_migrations().await?;
|
store.run_migrations().await?;
|
||||||
|
|
||||||
|
|||||||
+52
-22
@@ -5,7 +5,7 @@
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::{Seek, SeekFrom, Write};
|
use std::io::{Seek, SeekFrom, Write};
|
||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
use fs4::FileExt;
|
use fs4::FileExt;
|
||||||
@@ -94,17 +94,17 @@ fn safe_channel_key(channel: &str) -> Result<String, PairingStoreError> {
|
|||||||
Ok(safe)
|
Ok(safe)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pairing_path(base_dir: &PathBuf, channel: &str) -> Result<PathBuf, PairingStoreError> {
|
fn pairing_path(base_dir: &Path, channel: &str) -> Result<PathBuf, PairingStoreError> {
|
||||||
let key = safe_channel_key(channel)?;
|
let key = safe_channel_key(channel)?;
|
||||||
Ok(base_dir.join(format!("{}-pairing.json", key)))
|
Ok(base_dir.join(format!("{}-pairing.json", key)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn allow_from_path(base_dir: &PathBuf, channel: &str) -> Result<PathBuf, PairingStoreError> {
|
fn allow_from_path(base_dir: &Path, channel: &str) -> Result<PathBuf, PairingStoreError> {
|
||||||
let key = safe_channel_key(channel)?;
|
let key = safe_channel_key(channel)?;
|
||||||
Ok(base_dir.join(format!("{}-allowFrom.json", key)))
|
Ok(base_dir.join(format!("{}-allowFrom.json", key)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn approve_attempts_path(base_dir: &PathBuf, channel: &str) -> Result<PathBuf, PairingStoreError> {
|
fn approve_attempts_path(base_dir: &Path, channel: &str) -> Result<PathBuf, PairingStoreError> {
|
||||||
let key = safe_channel_key(channel)?;
|
let key = safe_channel_key(channel)?;
|
||||||
Ok(base_dir.join(format!("{}-approve-attempts.json", key)))
|
Ok(base_dir.join(format!("{}-approve-attempts.json", key)))
|
||||||
}
|
}
|
||||||
@@ -236,7 +236,8 @@ impl PairingStore {
|
|||||||
file.lock_exclusive()?;
|
file.lock_exclusive()?;
|
||||||
|
|
||||||
let content = fs::read_to_string(&path).unwrap_or_default();
|
let content = fs::read_to_string(&path).unwrap_or_default();
|
||||||
let mut store: PairingStoreFile = serde_json::from_str(&content).unwrap_or(PairingStoreFile {
|
let mut store: PairingStoreFile =
|
||||||
|
serde_json::from_str(&content).unwrap_or(PairingStoreFile {
|
||||||
version: 1,
|
version: 1,
|
||||||
requests: Vec::new(),
|
requests: Vec::new(),
|
||||||
});
|
});
|
||||||
@@ -296,7 +297,10 @@ impl PairingStore {
|
|||||||
self.write_pairing_file_locked(&mut file, channel, &store.requests)?;
|
self.write_pairing_file_locked(&mut file, channel, &store.requests)?;
|
||||||
fs4::FileExt::unlock(&file)?;
|
fs4::FileExt::unlock(&file)?;
|
||||||
|
|
||||||
Ok(UpsertResult { code, created: true })
|
Ok(UpsertResult {
|
||||||
|
code,
|
||||||
|
created: true,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_approve_rate_limited(&self, channel: &str) -> Result<bool, PairingStoreError> {
|
fn is_approve_rate_limited(&self, channel: &str) -> Result<bool, PairingStoreError> {
|
||||||
@@ -306,8 +310,7 @@ impl PairingStore {
|
|||||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(false),
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(false),
|
||||||
Err(e) => return Err(e.into()),
|
Err(e) => return Err(e.into()),
|
||||||
};
|
};
|
||||||
let mut data: ApproveAttemptsFile =
|
let mut data: ApproveAttemptsFile = serde_json::from_str(&content).unwrap_or_default();
|
||||||
serde_json::from_str(&content).unwrap_or_default();
|
|
||||||
let now = now_secs();
|
let now = now_secs();
|
||||||
let cutoff = now.saturating_sub(PAIRING_APPROVE_RATE_WINDOW_SECS);
|
let cutoff = now.saturating_sub(PAIRING_APPROVE_RATE_WINDOW_SECS);
|
||||||
data.failed_at.retain(|&t| t >= cutoff);
|
data.failed_at.retain(|&t| t >= cutoff);
|
||||||
@@ -321,11 +324,11 @@ impl PairingStore {
|
|||||||
.read(true)
|
.read(true)
|
||||||
.write(true)
|
.write(true)
|
||||||
.create(true)
|
.create(true)
|
||||||
|
.truncate(true)
|
||||||
.open(&path)?;
|
.open(&path)?;
|
||||||
file.lock_exclusive()?;
|
file.lock_exclusive()?;
|
||||||
let content = fs::read_to_string(&path).unwrap_or_default();
|
let content = fs::read_to_string(&path).unwrap_or_default();
|
||||||
let mut data: ApproveAttemptsFile =
|
let mut data: ApproveAttemptsFile = serde_json::from_str(&content).unwrap_or_default();
|
||||||
serde_json::from_str(&content).unwrap_or_default();
|
|
||||||
let now = now_secs();
|
let now = now_secs();
|
||||||
data.failed_at.push(now);
|
data.failed_at.push(now);
|
||||||
let cutoff = now.saturating_sub(PAIRING_APPROVE_RATE_WINDOW_SECS);
|
let cutoff = now.saturating_sub(PAIRING_APPROVE_RATE_WINDOW_SECS);
|
||||||
@@ -368,7 +371,8 @@ impl PairingStore {
|
|||||||
file.lock_exclusive()?;
|
file.lock_exclusive()?;
|
||||||
|
|
||||||
let content = fs::read_to_string(&path).unwrap_or_default();
|
let content = fs::read_to_string(&path).unwrap_or_default();
|
||||||
let mut store: PairingStoreFile = serde_json::from_str(&content).unwrap_or(PairingStoreFile {
|
let mut store: PairingStoreFile =
|
||||||
|
serde_json::from_str(&content).unwrap_or(PairingStoreFile {
|
||||||
version: 1,
|
version: 1,
|
||||||
requests: Vec::new(),
|
requests: Vec::new(),
|
||||||
});
|
});
|
||||||
@@ -409,7 +413,8 @@ impl PairingStore {
|
|||||||
Err(e) => return Err(e.into()),
|
Err(e) => return Err(e.into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let file: AllowFromStoreFile = serde_json::from_str(&content).unwrap_or(AllowFromStoreFile {
|
let file: AllowFromStoreFile =
|
||||||
|
serde_json::from_str(&content).unwrap_or(AllowFromStoreFile {
|
||||||
version: 1,
|
version: 1,
|
||||||
allow_from: Vec::new(),
|
allow_from: Vec::new(),
|
||||||
});
|
});
|
||||||
@@ -433,10 +438,9 @@ impl PairingStore {
|
|||||||
if let Some(u) = username {
|
if let Some(u) = username {
|
||||||
let u = u.trim().to_lowercase();
|
let u = u.trim().to_lowercase();
|
||||||
let u_norm = u.strip_prefix('@').unwrap_or(&u);
|
let u_norm = u.strip_prefix('@').unwrap_or(&u);
|
||||||
if allow
|
if allow.iter().any(|e| {
|
||||||
.iter()
|
e.trim().to_lowercase() == u || e.trim().to_lowercase() == format!("@{}", u_norm)
|
||||||
.any(|e| e.trim().to_lowercase() == u || e.trim().to_lowercase() == format!("@{}", u_norm))
|
}) {
|
||||||
{
|
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -456,6 +460,7 @@ impl PairingStore {
|
|||||||
.read(true)
|
.read(true)
|
||||||
.write(true)
|
.write(true)
|
||||||
.create(true)
|
.create(true)
|
||||||
|
.truncate(true)
|
||||||
.open(&path)?;
|
.open(&path)?;
|
||||||
|
|
||||||
file.lock_exclusive()?;
|
file.lock_exclusive()?;
|
||||||
@@ -563,11 +568,20 @@ mod tests {
|
|||||||
fn test_upsert_request_creates_new() {
|
fn test_upsert_request_creates_new() {
|
||||||
let (store, _) = test_store();
|
let (store, _) = test_store();
|
||||||
let result = store
|
let result = store
|
||||||
.upsert_request("telegram", "user123", Some(serde_json::json!({"chat_id": 456})))
|
.upsert_request(
|
||||||
|
"telegram",
|
||||||
|
"user123",
|
||||||
|
Some(serde_json::json!({"chat_id": 456})),
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(result.created);
|
assert!(result.created);
|
||||||
assert_eq!(result.code.len(), PAIRING_CODE_LENGTH);
|
assert_eq!(result.code.len(), PAIRING_CODE_LENGTH);
|
||||||
assert!(result.code.chars().all(|c| PAIRING_ALPHABET.contains(&(c as u8))));
|
assert!(
|
||||||
|
result
|
||||||
|
.code
|
||||||
|
.chars()
|
||||||
|
.all(|c| PAIRING_ALPHABET.contains(&(c as u8)))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -575,7 +589,9 @@ mod tests {
|
|||||||
let (store, _) = test_store();
|
let (store, _) = test_store();
|
||||||
let r1 = store.upsert_request("telegram", "user123", None).unwrap();
|
let r1 = store.upsert_request("telegram", "user123", None).unwrap();
|
||||||
assert!(r1.created);
|
assert!(r1.created);
|
||||||
let r2 = store.upsert_request("telegram", "user123", Some(serde_json::json!({"x": 1}))).unwrap();
|
let r2 = store
|
||||||
|
.upsert_request("telegram", "user123", Some(serde_json::json!({"x": 1})))
|
||||||
|
.unwrap();
|
||||||
assert!(!r2.created);
|
assert!(!r2.created);
|
||||||
assert_eq!(r1.code, r2.code);
|
assert_eq!(r1.code, r2.code);
|
||||||
|
|
||||||
@@ -633,21 +649,35 @@ mod tests {
|
|||||||
let r = store.upsert_request("telegram", "user999", None).unwrap();
|
let r = store.upsert_request("telegram", "user999", None).unwrap();
|
||||||
store.approve("telegram", &r.code).unwrap();
|
store.approve("telegram", &r.code).unwrap();
|
||||||
|
|
||||||
assert!(store.is_sender_allowed("telegram", "user999", None).unwrap());
|
assert!(
|
||||||
|
store
|
||||||
|
.is_sender_allowed("telegram", "user999", None)
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
assert!(!store.is_sender_allowed("telegram", "other", None).unwrap());
|
assert!(!store.is_sender_allowed("telegram", "other", None).unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_is_sender_allowed_by_username() {
|
fn test_is_sender_allowed_by_username() {
|
||||||
let (store, _) = test_store();
|
let (store, _) = test_store();
|
||||||
store.upsert_request("telegram", "alice", Some(serde_json::json!({"username": "alice"}))).unwrap();
|
store
|
||||||
|
.upsert_request(
|
||||||
|
"telegram",
|
||||||
|
"alice",
|
||||||
|
Some(serde_json::json!({"username": "alice"})),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
let pending = store.list_pending("telegram").unwrap();
|
let pending = store.list_pending("telegram").unwrap();
|
||||||
store.approve("telegram", &pending[0].code).unwrap();
|
store.approve("telegram", &pending[0].code).unwrap();
|
||||||
|
|
||||||
// approve adds id to allow_from. For username we need to add it manually.
|
// approve adds id to allow_from. For username we need to add it manually.
|
||||||
// Actually approve adds entry.id which is "alice". So is_sender_allowed("telegram", "alice", None) would work.
|
// Actually approve adds entry.id which is "alice". So is_sender_allowed("telegram", "alice", None) would work.
|
||||||
assert!(store.is_sender_allowed("telegram", "alice", None).unwrap());
|
assert!(store.is_sender_allowed("telegram", "alice", None).unwrap());
|
||||||
assert!(store.is_sender_allowed("telegram", "alice", Some("alice")).unwrap());
|
assert!(
|
||||||
|
store
|
||||||
|
.is_sender_allowed("telegram", "alice", Some("alice"))
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+8
-18
@@ -101,15 +101,13 @@ mod platform {
|
|||||||
let ss = SecretService::connect(EncryptionType::Dh)
|
let ss = SecretService::connect(EncryptionType::Dh)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
SecretError::KeychainError(format!(
|
SecretError::KeychainError(format!("Failed to connect to secret service: {}", e))
|
||||||
"Failed to connect to secret service: {}",
|
|
||||||
e
|
|
||||||
))
|
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let collection = ss.get_default_collection().await.map_err(|e| {
|
let collection = ss
|
||||||
SecretError::KeychainError(format!("Failed to get collection: {}", e))
|
.get_default_collection()
|
||||||
})?;
|
.await
|
||||||
|
.map_err(|e| SecretError::KeychainError(format!("Failed to get collection: {}", e)))?;
|
||||||
|
|
||||||
// Unlock if needed
|
// Unlock if needed
|
||||||
if collection.is_locked().await.unwrap_or(true) {
|
if collection.is_locked().await.unwrap_or(true) {
|
||||||
@@ -132,9 +130,7 @@ mod platform {
|
|||||||
"text/plain",
|
"text/plain",
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| SecretError::KeychainError(format!("Failed to create secret: {}", e)))?;
|
||||||
SecretError::KeychainError(format!("Failed to create secret: {}", e))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -144,10 +140,7 @@ mod platform {
|
|||||||
let ss = SecretService::connect(EncryptionType::Dh)
|
let ss = SecretService::connect(EncryptionType::Dh)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
SecretError::KeychainError(format!(
|
SecretError::KeychainError(format!("Failed to connect to secret service: {}", e))
|
||||||
"Failed to connect to secret service: {}",
|
|
||||||
e
|
|
||||||
))
|
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let items = ss
|
let items = ss
|
||||||
@@ -188,10 +181,7 @@ mod platform {
|
|||||||
let ss = SecretService::connect(EncryptionType::Dh)
|
let ss = SecretService::connect(EncryptionType::Dh)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
SecretError::KeychainError(format!(
|
SecretError::KeychainError(format!("Failed to connect to secret service: {}", e))
|
||||||
"Failed to connect to secret service: {}",
|
|
||||||
e
|
|
||||||
))
|
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let items = ss
|
let items = ss
|
||||||
|
|||||||
@@ -192,9 +192,10 @@ impl CreateSecretParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Where a credential should be injected in an HTTP request.
|
/// Where a credential should be injected in an HTTP request.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
pub enum CredentialLocation {
|
pub enum CredentialLocation {
|
||||||
/// Inject as Authorization header (e.g., "Bearer {secret}")
|
/// Inject as Authorization header (e.g., "Bearer {secret}")
|
||||||
|
#[default]
|
||||||
AuthorizationBearer,
|
AuthorizationBearer,
|
||||||
/// Inject as Authorization header with Basic auth
|
/// Inject as Authorization header with Basic auth
|
||||||
AuthorizationBasic { username: String },
|
AuthorizationBasic { username: String },
|
||||||
@@ -209,12 +210,6 @@ pub enum CredentialLocation {
|
|||||||
UrlPath { placeholder: String },
|
UrlPath { placeholder: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for CredentialLocation {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::AuthorizationBearer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Mapping from a secret name to where it should be injected.
|
/// Mapping from a secret name to where it should be injected.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct CredentialMapping {
|
pub struct CredentialMapping {
|
||||||
|
|||||||
+3
-6
@@ -782,12 +782,9 @@ impl SetupWizard {
|
|||||||
fn save_and_summarize(&mut self) -> Result<(), SetupError> {
|
fn save_and_summarize(&mut self) -> Result<(), SetupError> {
|
||||||
self.settings.onboard_completed = true;
|
self.settings.onboard_completed = true;
|
||||||
|
|
||||||
self.settings.save().map_err(|e| {
|
self.settings
|
||||||
SetupError::Io(std::io::Error::new(
|
.save()
|
||||||
std::io::ErrorKind::Other,
|
.map_err(|e| std::io::Error::other(format!("Failed to save settings: {}", e)))?;
|
||||||
format!("Failed to save settings: {}", e),
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
println!();
|
println!();
|
||||||
print_success("Configuration saved to ~/.ironclaw/");
|
print_success("Configuration saved to ~/.ironclaw/");
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ fn is_workspace_path(path: &str) -> bool {
|
|||||||
.and_then(|f| f.to_str())
|
.and_then(|f| f.to_str())
|
||||||
.unwrap_or(path);
|
.unwrap_or(path);
|
||||||
|
|
||||||
WORKSPACE_FILES.iter().any(|ws| *ws == filename)
|
WORKSPACE_FILES.contains(&filename)
|
||||||
|| path.starts_with("daily/")
|
|| path.starts_with("daily/")
|
||||||
|| path.starts_with("context/")
|
|| path.starts_with("context/")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -365,12 +365,7 @@ pub async fn save_mcp_servers_to_db(
|
|||||||
store
|
store
|
||||||
.set_setting(user_id, "mcp_servers", &value)
|
.set_setting(user_id, "mcp_servers", &value)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(std::io::Error::other)?;
|
||||||
ConfigError::Io(std::io::Error::new(
|
|
||||||
std::io::ErrorKind::Other,
|
|
||||||
e.to_string(),
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -422,7 +422,7 @@ impl Default for ToolRegistry {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::tools::tool::EchoTool;
|
use crate::tools::registry::EchoTool;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_register_and_get() {
|
async fn test_register_and_get() {
|
||||||
|
|||||||
+4
-4
@@ -199,6 +199,10 @@ pub trait Tool: Send + Sync {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
/// A simple no-op tool for testing.
|
/// A simple no-op tool for testing.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct EchoTool;
|
pub struct EchoTool;
|
||||||
@@ -246,10 +250,6 @@ impl Tool for EchoTool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_echo_tool() {
|
async fn test_echo_tool() {
|
||||||
let tool = EchoTool;
|
let tool = EchoTool;
|
||||||
|
|||||||
@@ -404,16 +404,15 @@ impl Repository {
|
|||||||
Vec::new()
|
Vec::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
let vector_results = if config.use_vector && embedding.is_some() {
|
let vector_results = if config.use_vector {
|
||||||
self.vector_search(
|
if let Some(embedding) = embedding {
|
||||||
user_id,
|
self.vector_search(user_id, agent_id, embedding, config.pre_fusion_limit)
|
||||||
agent_id,
|
|
||||||
embedding.unwrap(),
|
|
||||||
config.pre_fusion_limit,
|
|
||||||
)
|
|
||||||
.await?
|
.await?
|
||||||
} else {
|
} else {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(reciprocal_rank_fusion(fts_results, vector_results, config))
|
Ok(reciprocal_rank_fusion(fts_results, vector_results, config))
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
//! Verifies the full pairing lifecycle: upsert → list → approve → allowFrom → is_sender_allowed.
|
//! Verifies the full pairing lifecycle: upsert → list → approve → allowFrom → is_sender_allowed.
|
||||||
//! Uses temp directory for isolation.
|
//! Uses temp directory for isolation.
|
||||||
|
|
||||||
use ironclaw::cli::{run_pairing_command_with_store, PairingCommand};
|
use ironclaw::cli::{PairingCommand, run_pairing_command_with_store};
|
||||||
use ironclaw::pairing::PairingStore;
|
use ironclaw::pairing::PairingStore;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
||||||
@@ -19,10 +19,16 @@ fn test_pairing_flow_unknown_user_to_approved() {
|
|||||||
let channel = "telegram";
|
let channel = "telegram";
|
||||||
|
|
||||||
// 1. Unknown user sends first message -> upsert creates request
|
// 1. Unknown user sends first message -> upsert creates request
|
||||||
let r1 = store.upsert_request(channel, "user_12345", Some(serde_json::json!({
|
let r1 = store
|
||||||
|
.upsert_request(
|
||||||
|
channel,
|
||||||
|
"user_12345",
|
||||||
|
Some(serde_json::json!({
|
||||||
"chat_id": 999,
|
"chat_id": 999,
|
||||||
"username": "alice"
|
"username": "alice"
|
||||||
}))).unwrap();
|
})),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
assert!(r1.created);
|
assert!(r1.created);
|
||||||
assert!(!r1.code.is_empty());
|
assert!(!r1.code.is_empty());
|
||||||
assert_eq!(r1.code.len(), 8);
|
assert_eq!(r1.code.len(), 8);
|
||||||
@@ -34,7 +40,11 @@ fn test_pairing_flow_unknown_user_to_approved() {
|
|||||||
assert_eq!(pending[0].code, r1.code);
|
assert_eq!(pending[0].code, r1.code);
|
||||||
|
|
||||||
// 3. User is not allowed yet
|
// 3. User is not allowed yet
|
||||||
assert!(!store.is_sender_allowed(channel, "user_12345", Some("alice")).unwrap());
|
assert!(
|
||||||
|
!store
|
||||||
|
.is_sender_allowed(channel, "user_12345", Some("alice"))
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
// 4. Approve via code
|
// 4. Approve via code
|
||||||
let approved = store.approve(channel, &r1.code).unwrap();
|
let approved = store.approve(channel, &r1.code).unwrap();
|
||||||
@@ -42,8 +52,16 @@ fn test_pairing_flow_unknown_user_to_approved() {
|
|||||||
assert_eq!(approved.unwrap().id, "user_12345");
|
assert_eq!(approved.unwrap().id, "user_12345");
|
||||||
|
|
||||||
// 5. User is now allowed
|
// 5. User is now allowed
|
||||||
assert!(store.is_sender_allowed(channel, "user_12345", None).unwrap());
|
assert!(
|
||||||
assert!(store.is_sender_allowed(channel, "user_12345", Some("alice")).unwrap());
|
store
|
||||||
|
.is_sender_allowed(channel, "user_12345", None)
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
store
|
||||||
|
.is_sender_allowed(channel, "user_12345", Some("alice"))
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
// 6. Pending list is empty
|
// 6. Pending list is empty
|
||||||
let pending_after = store.list_pending(channel).unwrap();
|
let pending_after = store.list_pending(channel).unwrap();
|
||||||
@@ -69,7 +87,11 @@ fn test_pairing_flow_cli_approve() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
assert!(store.is_sender_allowed("telegram", "user_999", None).unwrap());
|
assert!(
|
||||||
|
store
|
||||||
|
.is_sender_allowed("telegram", "user_999", None)
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -109,4 +131,3 @@ fn test_pairing_multiple_channels_isolated() {
|
|||||||
store.approve("slack", &r_slack.code).unwrap();
|
store.approve("slack", &r_slack.code).unwrap();
|
||||||
assert!(store.is_sender_allowed("slack", "user_b", None).unwrap());
|
assert!(store.is_sender_allowed("slack", "user_b", None).unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ use std::collections::HashMap;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use ironclaw::channels::Channel;
|
use ironclaw::channels::Channel;
|
||||||
use ironclaw::pairing::PairingStore;
|
|
||||||
use ironclaw::channels::wasm::{
|
use ironclaw::channels::wasm::{
|
||||||
ChannelCapabilities, EmitRateLimitConfig, PreparedChannelModule, RegisteredEndpoint,
|
ChannelCapabilities, EmitRateLimitConfig, PreparedChannelModule, RegisteredEndpoint,
|
||||||
WasmChannel, WasmChannelRouter, WasmChannelRuntime, WasmChannelRuntimeConfig,
|
WasmChannel, WasmChannelRouter, WasmChannelRuntime, WasmChannelRuntimeConfig,
|
||||||
};
|
};
|
||||||
|
use ironclaw::pairing::PairingStore;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
||||||
/// Create a test runtime for WASM channel operations.
|
/// Create a test runtime for WASM channel operations.
|
||||||
|
|||||||
+228
@@ -0,0 +1,228 @@
|
|||||||
|
<?xml version='1.0' encoding='windows-1252'?>
|
||||||
|
<!--
|
||||||
|
Copyright (C) 2017 Christopher R. Field.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!--
|
||||||
|
The "cargo wix" subcommand provides a variety of predefined variables available
|
||||||
|
for customization of this template. The values for each variable are set at
|
||||||
|
installer creation time. The following variables are available:
|
||||||
|
|
||||||
|
TargetTriple = The rustc target triple name.
|
||||||
|
TargetEnv = The rustc target environment. This is typically either
|
||||||
|
"msvc" or "gnu" depending on the toolchain downloaded and
|
||||||
|
installed.
|
||||||
|
TargetVendor = The rustc target vendor. This is typically "pc", but Rust
|
||||||
|
does support other vendors, like "uwp".
|
||||||
|
CargoTargetBinDir = The complete path to the directory containing the
|
||||||
|
binaries (exes) to include. The default would be
|
||||||
|
"target\release\". If an explicit rustc target triple is
|
||||||
|
used, i.e. cross-compiling, then the default path would
|
||||||
|
be "target\<CARGO_TARGET>\<CARGO_PROFILE>",
|
||||||
|
where "<CARGO_TARGET>" is replaced with the "CargoTarget"
|
||||||
|
variable value and "<CARGO_PROFILE>" is replaced with the
|
||||||
|
value from the "CargoProfile" variable. This can also
|
||||||
|
be overridden manually with the "target-bin-dir" flag.
|
||||||
|
CargoTargetDir = The path to the directory for the build artifacts, i.e.
|
||||||
|
"target".
|
||||||
|
CargoProfile = The cargo profile used to build the binaries
|
||||||
|
(usually "debug" or "release").
|
||||||
|
Version = The version for the installer. The default is the
|
||||||
|
"Major.Minor.Fix" semantic versioning number of the Rust
|
||||||
|
package.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Please do not remove these pre-processor If-Else blocks. These are used with
|
||||||
|
the `cargo wix` subcommand to automatically determine the installation
|
||||||
|
destination for 32-bit versus 64-bit installers. Removal of these lines will
|
||||||
|
cause installation errors.
|
||||||
|
-->
|
||||||
|
<?if $(sys.BUILDARCH) = x64 or $(sys.BUILDARCH) = arm64 ?>
|
||||||
|
<?define PlatformProgramFilesFolder = "ProgramFiles64Folder" ?>
|
||||||
|
<?else ?>
|
||||||
|
<?define PlatformProgramFilesFolder = "ProgramFilesFolder" ?>
|
||||||
|
<?endif ?>
|
||||||
|
|
||||||
|
<Wix xmlns='http://schemas.microsoft.com/wix/2006/wi'>
|
||||||
|
|
||||||
|
<Product
|
||||||
|
Id='*'
|
||||||
|
Name='ironclaw'
|
||||||
|
UpgradeCode='D0156E61-BA37-451E-8AB9-1A2ECCCFA48F'
|
||||||
|
Manufacturer='NEAR AI'
|
||||||
|
Language='1033'
|
||||||
|
Codepage='1252'
|
||||||
|
Version='$(var.Version)'>
|
||||||
|
|
||||||
|
<Package Id='*'
|
||||||
|
Keywords='Installer'
|
||||||
|
Description='Secure personal AI assistant that protects your data and expands its capabilities on the fly'
|
||||||
|
Manufacturer='NEAR AI'
|
||||||
|
InstallerVersion='450'
|
||||||
|
Languages='1033'
|
||||||
|
Compressed='yes'
|
||||||
|
InstallScope='perMachine'
|
||||||
|
SummaryCodepage='1252'
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MajorUpgrade
|
||||||
|
Schedule='afterInstallInitialize'
|
||||||
|
DowngradeErrorMessage='A newer version of [ProductName] is already installed. Setup will now exit.'/>
|
||||||
|
|
||||||
|
<Media Id='1' Cabinet='media1.cab' EmbedCab='yes' DiskPrompt='CD-ROM #1'/>
|
||||||
|
<Property Id='DiskPrompt' Value='ironclaw Installation'/>
|
||||||
|
|
||||||
|
<Directory Id='TARGETDIR' Name='SourceDir'>
|
||||||
|
<Directory Id='$(var.PlatformProgramFilesFolder)' Name='PFiles'>
|
||||||
|
<Directory Id='APPLICATIONFOLDER' Name='ironclaw'>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Enabling the license sidecar file in the installer is a four step process:
|
||||||
|
|
||||||
|
1. Uncomment the `Component` tag and its contents.
|
||||||
|
2. Change the value for the `Source` attribute in the `File` tag to a path
|
||||||
|
to the file that should be included as the license sidecar file. The path
|
||||||
|
can, and probably should be, relative to this file.
|
||||||
|
3. Change the value for the `Name` attribute in the `File` tag to the
|
||||||
|
desired name for the file when it is installed alongside the `bin` folder
|
||||||
|
in the installation directory. This can be omitted if the desired name is
|
||||||
|
the same as the file name.
|
||||||
|
4. Uncomment the `ComponentRef` tag with the Id attribute value of "License"
|
||||||
|
further down in this file.
|
||||||
|
-->
|
||||||
|
<!--
|
||||||
|
<Component Id='License' Guid='*'>
|
||||||
|
<File Id='LicenseFile' Name='ChangeMe' DiskId='1' Source='C:\Path\To\File' KeyPath='yes'/>
|
||||||
|
</Component>
|
||||||
|
-->
|
||||||
|
|
||||||
|
<Directory Id='Bin' Name='bin'>
|
||||||
|
<Component Id='Path' Guid='F90B6EA6-87F7-499B-BB19-CF55DE1EB339' KeyPath='yes'>
|
||||||
|
<Environment
|
||||||
|
Id='PATH'
|
||||||
|
Name='PATH'
|
||||||
|
Value='[Bin]'
|
||||||
|
Permanent='no'
|
||||||
|
Part='last'
|
||||||
|
Action='set'
|
||||||
|
System='yes'/>
|
||||||
|
</Component>
|
||||||
|
<Component Id='binary0' Guid='*'>
|
||||||
|
<File
|
||||||
|
Id='exe0'
|
||||||
|
Name='ironclaw.exe'
|
||||||
|
DiskId='1'
|
||||||
|
Source='$(var.CargoTargetBinDir)\ironclaw.exe'
|
||||||
|
KeyPath='yes'/>
|
||||||
|
</Component>
|
||||||
|
</Directory>
|
||||||
|
</Directory>
|
||||||
|
</Directory>
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
<Feature
|
||||||
|
Id='Binaries'
|
||||||
|
Title='Application'
|
||||||
|
Description='Installs all binaries and the license.'
|
||||||
|
Level='1'
|
||||||
|
ConfigurableDirectory='APPLICATIONFOLDER'
|
||||||
|
AllowAdvertise='no'
|
||||||
|
Display='expand'
|
||||||
|
Absent='disallow'>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Uncomment the following `ComponentRef` tag to add the license
|
||||||
|
sidecar file to the installer.
|
||||||
|
-->
|
||||||
|
<!--<ComponentRef Id='License'/>-->
|
||||||
|
|
||||||
|
<ComponentRef Id='binary0'/>
|
||||||
|
|
||||||
|
<Feature
|
||||||
|
Id='Environment'
|
||||||
|
Title='PATH Environment Variable'
|
||||||
|
Description='Add the install location of the [ProductName] executable to the PATH system environment variable. This allows the [ProductName] executable to be called from any location.'
|
||||||
|
Level='1'
|
||||||
|
Absent='allow'>
|
||||||
|
<ComponentRef Id='Path'/>
|
||||||
|
</Feature>
|
||||||
|
</Feature>
|
||||||
|
|
||||||
|
<SetProperty Id='ARPINSTALLLOCATION' Value='[APPLICATIONFOLDER]' After='CostFinalize'/>
|
||||||
|
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Uncomment the following `Icon` and `Property` tags to change the product icon.
|
||||||
|
|
||||||
|
The product icon is the graphic that appears in the Add/Remove
|
||||||
|
Programs control panel for the application.
|
||||||
|
-->
|
||||||
|
<!--<Icon Id='ProductICO' SourceFile='wix\Product.ico'/>-->
|
||||||
|
<!--<Property Id='ARPPRODUCTICON' Value='ProductICO' />-->
|
||||||
|
|
||||||
|
<Property Id='ARPHELPLINK' Value='https://github.com/nearai/ironclaw'/>
|
||||||
|
|
||||||
|
<UI>
|
||||||
|
<UIRef Id='WixUI_FeatureTree'/>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Enabling the EULA dialog in the installer is a three step process:
|
||||||
|
|
||||||
|
1. Comment out or remove the two `Publish` tags that follow the
|
||||||
|
`WixVariable` tag.
|
||||||
|
2. Uncomment the `<WixVariable Id='WixUILicenseRtf' Value='Path\to\Eula.rft'>` tag further down
|
||||||
|
3. Replace the `Value` attribute of the `WixVariable` tag with
|
||||||
|
the path to a RTF file that will be used as the EULA and
|
||||||
|
displayed in the license agreement dialog.
|
||||||
|
-->
|
||||||
|
<Publish Dialog='WelcomeDlg' Control='Next' Event='NewDialog' Value='CustomizeDlg' Order='99'>1</Publish>
|
||||||
|
<Publish Dialog='CustomizeDlg' Control='Back' Event='NewDialog' Value='WelcomeDlg' Order='99'>1</Publish>
|
||||||
|
|
||||||
|
</UI>
|
||||||
|
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Enabling the EULA dialog in the installer requires uncommenting
|
||||||
|
the following `WixUILicenseRTF` tag and changing the `Value`
|
||||||
|
attribute.
|
||||||
|
-->
|
||||||
|
<!-- <WixVariable Id='WixUILicenseRtf' Value='Relative\Path\to\Eula.rtf'/> -->
|
||||||
|
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Uncomment the next `WixVariable` tag to customize the installer's
|
||||||
|
Graphical User Interface (GUI) and add a custom banner image across
|
||||||
|
the top of each screen. See the WiX Toolset documentation for details
|
||||||
|
about customization.
|
||||||
|
|
||||||
|
The banner BMP dimensions are 493 x 58 pixels.
|
||||||
|
-->
|
||||||
|
<!--<WixVariable Id='WixUIBannerBmp' Value='wix\Banner.bmp'/>-->
|
||||||
|
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Uncomment the next `WixVariable` tag to customize the installer's
|
||||||
|
Graphical User Interface (GUI) and add a custom image to the first
|
||||||
|
dialog, or screen. See the WiX Toolset documentation for details about
|
||||||
|
customization.
|
||||||
|
|
||||||
|
The dialog BMP dimensions are 493 x 312 pixels.
|
||||||
|
-->
|
||||||
|
<!--<WixVariable Id='WixUIDialogBmp' Value='wix\Dialog.bmp'/>-->
|
||||||
|
|
||||||
|
</Product>
|
||||||
|
|
||||||
|
</Wix>
|
||||||
Reference in New Issue
Block a user