Skip to content

Commit 8cbe3bc

Browse files
feat(cli): wire runtime, security, ux, protocol and plugin surfaces (#39)
* feat(cli): wire runtime, security, ux, protocol and plugin surfaces Implement the capability lots from the feature audit across seven areas, then wire them together so declared surfaces reach a real executor. Runtime and protocols: server-owned tool results are never re-executed locally, incomplete streams and unsuccessful terminal events never report success, sessions are origin-bound with persistent multi-turn JSON-RPC and event-independent deadlines. MCP gains async stdio plus Streamable HTTP with OAuth keyring reuse; the app server shares one live-session registry with independent WebSocket and SSE subscribers and returns 501 for unsupported compatibility endpoints. Execution security: one authorization gate covers direct, Batch, child and plugin paths with exact single-use approvals, workspace confinement, traversal and symlink checks, final environment filtering, bounded capture and controlled process-group cancellation. Interactive sessions now honor --sandbox, --ask-for-approval, --full-auto, --dangerously-bypass and --add-dir instead of ignoring them, and the TUI and agent executors build an explicit authorized context. Policy evaluation and process-group termination run in-process; the Linux sandbox wrapper is the binary itself when no sibling helper is installed, and an unregistered host still fails closed. UX and sessions: a shared local store backs persisted new, resume and fork, durable rename, favorite and protection, JSON and YAML export and import, and conversation-scoped undo and redo, with unified dispatch across CLI and TUI. Plugins gain an explicit ABI, activation and packaging contract with a Node runtime. Platform work aligns the opentelemetry family, corrects release asset names, and makes installers validate URLs, architecture, checksums and bounds with staging and rollback. Config overrides for sandbox mode and writable roots are now applied, shutdown no longer deletes other sessions' files, and --debug writes a content-free local journal instead of a full trace dump. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> * ci: provision node for plugin tests and fix bsd version parsing The distribution matrix added macOS runners, where BSD sed does not accept the GNU `\s` class, so the workspace version was captured as the whole `version = "0.1.8"` line and never matched VERSION_CLI. Use a POSIX character class instead. Node plugin tests launch a real `node` worker; the test and coverage jobs had no Node, so eight tests failed on a missing runtime rather than on behavior. Provision Node 22 in both jobs and in the dev container, and state the prerequisite in the development guide, instead of skipping the tests. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> * fix(install): install on case-insensitive volumes and drop gnu-only stat On macOS and Windows the filesystem folds case, so the alias path "cortex" resolves to the destination "Cortex" itself. The installer read that as a foreign command already occupying the name and refused to install at all. Skip alias names that are the destination file, and keep refusing a real unrelated binary, now covered by a regression test that runs on a case-insensitive volume plus one that always runs. The release metadata fixture also used GNU `stat -c%s`, which BSD stat rejects; `wc -c` is portable. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> * fix(plugins): resolve node before clearing the worker environment Keep the resolved executable for cleanup and cover custom PATH execution with a real Node child. Verify installer aliases on case-folding filesystems. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> * fix(tests): resolve node consistently in cli plugin fixtures Use the runtime resolver when detecting supported Node installations so custom-PATH builds are not incorrectly expected to fail. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --------- Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
1 parent 6282f9a commit 8cbe3bc

258 files changed

Lines changed: 30505 additions & 17347 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.devcontainer/devcontainer.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
{
22
"name": "Cortex Rust workspace",
33
"build": {"dockerfile": "Dockerfile", "context": ".."},
4+
"features": {
5+
"ghcr.io/devcontainers/features/node:1": {"version": "22"}
6+
},
47
"remoteUser": "cortex",
58
"updateRemoteUserUID": true,
69
"postCreateCommand": "bash scripts/dev-setup.sh",

.github/workflows/ci.yml

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,23 @@ jobs:
5454
retention-days: 14
5555

5656
version-check:
57-
name: CLI Version Check
58-
runs-on: ubuntu-latest
57+
name: CLI Version and Distribution (${{ matrix.os }})
58+
runs-on: ${{ matrix.os }}
59+
strategy:
60+
fail-fast: false
61+
matrix:
62+
os: [ubuntu-latest, macos-latest, macos-15-intel, windows-latest]
5963
steps:
6064
- uses: actions/checkout@v7
65+
- uses: actions/setup-python@v5
66+
with:
67+
python-version: '3.12'
68+
- run: python -m pip install -r scripts/readiness/requirements.txt
6169
- name: Verify CLI version consistency
70+
shell: bash
6271
run: ./scripts/check-cli-version.sh
72+
- name: Verify installer fixtures and package contracts
73+
run: python -B -m unittest discover -s scripts/readiness -p test_distribution.py -v
6374
- name: Verify Windows installer arch detection
6475
shell: pwsh
6576
run: ./scripts/test-install-ps1.ps1
@@ -103,6 +114,9 @@ jobs:
103114
sudo apt-get update
104115
sudo apt-get install -y libasound2-dev libssl-dev pkg-config git ripgrep
105116
- uses: dtolnay/rust-toolchain@1.98.0
117+
- uses: actions/setup-node@v4
118+
with:
119+
node-version: '22'
106120
- uses: Swatinem/rust-cache@v2
107121
with:
108122
prefix-key: rust-test
@@ -147,6 +161,9 @@ jobs:
147161
- uses: dtolnay/rust-toolchain@1.98.0
148162
with:
149163
components: llvm-tools-preview
164+
- uses: actions/setup-node@v4
165+
with:
166+
node-version: '22'
150167
- uses: Swatinem/rust-cache@v2
151168
with:
152169
prefix-key: rust-coverage

.github/workflows/homebrew.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ jobs:
3131
with:
3232
formula-name: cortex
3333
homebrew-tap: CortexLM/homebrew-tap
34-
download-url: https://github.com/${{ github.repository }}/releases/download/${{ steps.tag.outputs.tag }}/cortex-macos-arm64.tar.gz
34+
download-url: https://github.com/${{ github.repository }}/releases/download/${{ steps.tag.outputs.tag }}/cortex-cli-macos-arm64.tar.gz
3535
commit-message: |
3636
{{formulaName}} {{version}}
3737

.github/workflows/publish-r2.yml

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@ jobs:
129129
EXPECTED_ARTIFACTS=(
130130
"cortex-cli-linux-x64"
131131
"cortex-cli-linux-arm64"
132+
"cortex-cli-linux-x64-static"
133+
"cortex-cli-linux-arm64-static"
132134
"cortex-cli-macos-x64"
133135
"cortex-cli-macos-arm64"
134136
"cortex-cli-windows-x64"
@@ -152,11 +154,11 @@ jobs:
152154
run: |
153155
mkdir -p dist
154156
155-
for mapping in linux-x86_64:cortex-cli-linux-x64 linux-aarch64:cortex-cli-linux-arm64 darwin-x86_64:cortex-cli-macos-x64 darwin-aarch64:cortex-cli-macos-arm64; do
157+
for mapping in linux-x86_64:cortex-cli-linux-x64 linux-aarch64:cortex-cli-linux-arm64 linux-x86_64-musl:cortex-cli-linux-x64-static linux-aarch64-musl:cortex-cli-linux-arm64-static darwin-x86_64:cortex-cli-macos-x64 darwin-aarch64:cortex-cli-macos-arm64; do
156158
IFS=':' read -r platform artifact <<< "$mapping"
157159
158-
ARCHIVE=$(find "artifacts/$artifact" -name "*.tar.gz" | head -1)
159-
if [ -z "$ARCHIVE" ]; then
160+
ARCHIVE="artifacts/$artifact/$artifact.tar.gz"
161+
if [ ! -f "$ARCHIVE" ]; then
160162
echo "ERROR: No .tar.gz found in artifacts/$artifact"
161163
ls -la "artifacts/$artifact/"
162164
exit 1
@@ -171,8 +173,8 @@ jobs:
171173
for mapping in windows-x86_64:cortex-cli-windows-x64; do
172174
IFS=':' read -r platform artifact <<< "$mapping"
173175
174-
ARCHIVE=$(find "artifacts/$artifact" -name "*.zip" | head -1)
175-
if [ -z "$ARCHIVE" ]; then
176+
ARCHIVE="artifacts/$artifact/$artifact.zip"
177+
if [ ! -f "$ARCHIVE" ]; then
176178
echo "ERROR: No .zip found in artifacts/$artifact"
177179
ls -la "artifacts/$artifact/"
178180
exit 1
@@ -205,7 +207,8 @@ jobs:
205207
local sha
206208
sha=$(awk '{print $1}' "dist/$platform/${file}.sha256")
207209
local size
208-
size=$(stat -c%s "dist/$platform/$file")
210+
# wc -c is portable; GNU stat -c is unavailable on BSD/macOS.
211+
size=$(wc -c < "dist/$platform/$file" | tr -d '[:space:]')
209212
jq -n --arg url "$url" --arg sha256 "$sha" --argjson size "$size" \
210213
'{url: $url, sha256: $sha256, size: $size}'
211214
}
@@ -218,6 +221,8 @@ jobs:
218221
--arg release_notes "$RELEASE_NOTES" \
219222
--argjson linux_x86_64 "$(asset_obj linux-x86_64 cortex.tar.gz)" \
220223
--argjson linux_aarch64 "$(asset_obj linux-aarch64 cortex.tar.gz)" \
224+
--argjson linux_x86_64_musl "$(asset_obj linux-x86_64-musl cortex.tar.gz)" \
225+
--argjson linux_aarch64_musl "$(asset_obj linux-aarch64-musl cortex.tar.gz)" \
221226
--argjson darwin_x86_64 "$(asset_obj darwin-x86_64 cortex.tar.gz)" \
222227
--argjson darwin_aarch64 "$(asset_obj darwin-aarch64 cortex.tar.gz)" \
223228
--argjson windows_x86_64 "$(asset_obj windows-x86_64 cortex.zip)" \
@@ -230,6 +235,8 @@ jobs:
230235
assets: {
231236
"linux-x86_64": $linux_x86_64,
232237
"linux-aarch64": $linux_aarch64,
238+
"linux-x86_64-musl": $linux_x86_64_musl,
239+
"linux-aarch64-musl": $linux_aarch64_musl,
233240
"darwin-x86_64": $darwin_x86_64,
234241
"darwin-aarch64": $darwin_aarch64,
235242
"windows-x86_64": $windows_x86_64
@@ -271,7 +278,7 @@ jobs:
271278
rclone copyto "$src" "$REMOTE/$dest" --progress
272279
}
273280
274-
for platform in linux-x86_64 linux-aarch64 darwin-x86_64 darwin-aarch64; do
281+
for platform in linux-x86_64 linux-aarch64 linux-x86_64-musl linux-aarch64-musl darwin-x86_64 darwin-aarch64; do
275282
echo "Uploading $platform..."
276283
r2_copy "dist/$platform/cortex.tar.gz" "v1/assets/$platform/$VERSION/cortex.tar.gz"
277284
r2_copy "dist/$platform/cortex.tar.gz.sha256" "v1/assets/$platform/$VERSION/cortex.tar.gz.sha256"

.github/workflows/release.yml

Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ jobs:
237237
# =========================================================================
238238
- name: Build release binary (dynamic)
239239
if: matrix.static == false
240-
run: cargo +nightly build --release --target ${{ matrix.target }} -p cortex-cli
240+
run: cargo +nightly build --locked --release --target ${{ matrix.target }} -p cortex-cli
241241
env:
242242
RUSTFLAGS: ""
243243
CARGO_PROFILE_RELEASE_LTO: thin
@@ -246,7 +246,7 @@ jobs:
246246
if: matrix.target == 'x86_64-unknown-linux-musl'
247247
run: |
248248
# Build without audio feature (rodio/alsa) for musl - uses terminal bell fallback
249-
cargo +nightly build --release --target ${{ matrix.target }} -p cortex-cli --no-default-features --features cortex-tui
249+
cargo +nightly build --locked --release --target ${{ matrix.target }} -p cortex-cli --no-default-features --features cortex-tui
250250
env:
251251
RUSTFLAGS: "-Zthreads=32 -C target-feature=+crt-static"
252252
CARGO_PROFILE_RELEASE_LTO: thin
@@ -259,7 +259,7 @@ jobs:
259259
run: |
260260
# Build without audio feature (rodio/alsa) for musl - uses terminal bell fallback
261261
# Using native musl-gcc since we're on an ARM64 runner building for ARM64 target
262-
cargo +nightly build --release --target ${{ matrix.target }} -p cortex-cli --no-default-features --features cortex-tui
262+
cargo +nightly build --locked --release --target ${{ matrix.target }} -p cortex-cli --no-default-features --features cortex-tui
263263
env:
264264
RUSTFLAGS: "-Zthreads=32 -C target-feature=+crt-static"
265265
CARGO_PROFILE_RELEASE_LTO: thin
@@ -273,22 +273,17 @@ jobs:
273273
- name: Verify static binary
274274
if: matrix.static == true
275275
run: |
276-
echo "=== Verifying static binary ==="
276+
set -euo pipefail
277277
BINARY="target/${{ matrix.target }}/release/${{ env.BINARY_NAME }}"
278278
file "$BINARY"
279-
echo ""
280-
echo "=== Checking dynamic dependencies ==="
281-
# For a truly static binary, ldd should report "not a dynamic executable"
282-
# or show no dynamic dependencies
283-
if ldd "$BINARY" 2>&1 | grep -q "not a dynamic executable\|statically linked"; then
284-
echo "✅ Binary is statically linked"
285-
else
286-
echo "⚠️ Binary has some dynamic dependencies (expected for musl with linux-keyutils):"
287-
ldd "$BINARY" 2>&1 || true
279+
# readelf is deterministic for static PIE too; ldd text is not.
280+
readelf -l "$BINARY" > program-headers.txt
281+
readelf -d "$BINARY" > dynamic-headers.txt
282+
if grep -q INTERP program-headers.txt || grep -q NEEDED dynamic-headers.txt; then
283+
echo "::error::musl artifact is dynamically linked"
284+
exit 1
288285
fi
289-
echo ""
290-
echo "=== Binary size ==="
291-
ls -lh "$BINARY"
286+
"$BINARY" --version
292287
293288
- name: Prepare artifact (Unix)
294289
if: runner.os != 'Windows'
@@ -313,7 +308,7 @@ jobs:
313308
path: |
314309
${{ matrix.artifact }}.tar.gz
315310
${{ matrix.artifact }}.zip
316-
if-no-files-found: ignore
311+
if-no-files-found: error
317312

318313
# ==========================================================================
319314
# Create GitHub Release (lightweight - 4 vCPU)
@@ -341,14 +336,22 @@ jobs:
341336

342337
- name: Flatten and generate checksums
343338
run: |
344-
cd artifacts
345-
find . -type f \( -name "*.tar.gz" -o -name "*.zip" -o -name "*.dmg" -o -name "*.msi" -o -name "*.AppImage" \) -exec mv {} . \;
346-
rm -rf cortex-*/ || true
347-
sha256sum * > checksums-sha256.txt 2>/dev/null || true
348-
echo "=== Release artifacts ==="
349-
ls -la
350-
echo "=== Checksums ==="
351-
cat checksums-sha256.txt
339+
set -euo pipefail
340+
mkdir release-assets
341+
for artifact in artifacts/*; do
342+
[ -d "$artifact" ] || { echo "Unexpected artifact layout"; exit 1; }
343+
name=$(basename "$artifact")
344+
case "$name" in
345+
cortex-cli-windows-x64) ext=zip ;;
346+
cortex-cli-macos-x64|cortex-cli-macos-arm64|cortex-cli-linux-x64|cortex-cli-linux-arm64|cortex-cli-linux-x64-static|cortex-cli-linux-arm64-static) ext=tar.gz ;;
347+
*) echo "Unexpected artifact: $name"; exit 1 ;;
348+
esac
349+
cp "$artifact/$name.$ext" release-assets/
350+
done
351+
[ "$(find release-assets -type f | wc -l)" -eq 7 ]
352+
cd release-assets
353+
sha256sum -- *.tar.gz *.zip > checksums-sha256.txt
354+
sha256sum --check checksums-sha256.txt
352355
353356
- name: Determine release name
354357
id: release_name
@@ -370,7 +373,7 @@ jobs:
370373
prerelease: ${{ contains(needs.prepare.outputs.version, '-') }}
371374
generate_release_notes: true
372375
files: |
373-
artifacts/*
376+
release-assets/*
374377
env:
375378
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
376379

.github/workflows/winget.yml

Lines changed: 15 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -40,24 +40,13 @@ jobs:
4040
$baseUrl = "https://github.com/${{ github.repository }}/releases/download/$tag"
4141
4242
# Download and hash x64
43-
$x64Url = "$baseUrl/cortex-windows-x64.zip"
43+
$x64Url = "$baseUrl/cortex-cli-windows-x64.zip"
4444
Invoke-WebRequest -Uri $x64Url -OutFile "cortex-x64.zip" -ErrorAction Stop
4545
$x64Hash = (Get-FileHash -Path "cortex-x64.zip" -Algorithm SHA256).Hash
4646
47-
# Download and hash arm64
48-
$arm64Url = "$baseUrl/cortex-windows-arm64.zip"
49-
try {
50-
Invoke-WebRequest -Uri $arm64Url -OutFile "cortex-arm64.zip" -ErrorAction Stop
51-
$arm64Hash = (Get-FileHash -Path "cortex-arm64.zip" -Algorithm SHA256).Hash
52-
} catch {
53-
$arm64Hash = ""
54-
$arm64Url = ""
55-
}
56-
47+
# ARM64 is intentionally absent from the release matrix.
5748
echo "x64_url=$x64Url" >> $env:GITHUB_OUTPUT
5849
echo "x64_hash=$x64Hash" >> $env:GITHUB_OUTPUT
59-
echo "arm64_url=$arm64Url" >> $env:GITHUB_OUTPUT
60-
echo "arm64_hash=$arm64Hash" >> $env:GITHUB_OUTPUT
6150
6251
- name: Create winget manifest directory
6352
shell: pwsh
@@ -99,15 +88,6 @@ jobs:
9988
InstallerSha256: ${{ steps.release.outputs.x64_hash }}
10089
"@
10190
102-
if ("${{ steps.release.outputs.arm64_hash }}" -ne "") {
103-
$installers += @"
104-
105-
- Architecture: arm64
106-
InstallerUrl: ${{ steps.release.outputs.arm64_url }}
107-
InstallerSha256: ${{ steps.release.outputs.arm64_hash }}
108-
"@
109-
}
110-
11191
$installers += @"
11292
11393
ManifestType: installer
@@ -125,11 +105,11 @@ jobs:
125105
PackageLocale: en-US
126106
Publisher: CortexLM
127107
PublisherUrl: https://github.com/CortexLM
128-
PublisherSupportUrl: https://github.com/CortexLM/cortex-cli/issues
108+
PublisherSupportUrl: https://github.com/CortexLM/cli/issues
129109
PackageName: Cortex CLI
130-
PackageUrl: https://github.com/CortexLM/cortex-cli
110+
PackageUrl: https://github.com/CortexLM/cli
131111
License: Apache-2.0
132-
LicenseUrl: https://github.com/CortexLM/cortex-cli/blob/master/LICENSE
112+
LicenseUrl: https://github.com/CortexLM/cli/blob/main/LICENSE
133113
ShortDescription: Cortex CLI - A modern AI coding agent
134114
Description: Cortex is a powerful command-line AI coding assistant that helps developers write, debug, and maintain code more efficiently.
135115
Tags:
@@ -143,20 +123,26 @@ jobs:
143123
"@
144124
$content | Out-File -FilePath "${{ env.MANIFEST_DIR }}/${{ env.PACKAGE_ID }}.locale.en-US.yaml" -Encoding utf8
145125
146-
- name: Install wingetcreate
126+
- name: Require provisioned package tooling
147127
shell: pwsh
148128
run: |
149-
iwr https://aka.ms/wingetcreate/latest -OutFile wingetcreate.exe
129+
# Do not execute an unpinned executable downloaded at publish time.
130+
# The Windows runner image must provision the reviewed tooling.
131+
$tool = Get-Command wingetcreate.exe -CommandType Application -ErrorAction Stop
132+
$signature = Get-AuthenticodeSignature -FilePath $tool.Source
133+
if ($signature.Status -ne 'Valid' -or $signature.SignerCertificate.Subject -notmatch 'O=Microsoft Corporation') {
134+
throw 'wingetcreate must have a valid Microsoft signature'
135+
}
150136
151137
- name: Validate manifests
152138
shell: pwsh
153139
run: |
154-
.\wingetcreate.exe validate ${{ env.MANIFEST_DIR }}
140+
wingetcreate.exe validate ${{ env.MANIFEST_DIR }}
155141
156142
- name: Submit to winget-pkgs
157143
shell: pwsh
158144
run: |
159-
.\wingetcreate.exe submit ${{ env.MANIFEST_DIR }} --token ${{ secrets.WINGET_PAT }}
145+
wingetcreate.exe submit ${{ env.MANIFEST_DIR }} --token $env:WINGET_PAT
160146
env:
161147
WINGET_PAT: ${{ secrets.WINGET_PAT }}
162148

0 commit comments

Comments
 (0)