Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
fc46ac4
feat: add versioned local IPC
domenkozar Aug 21, 2026
d4366d8
fix(ipc): harden expiry metadata and framing
domenkozar Aug 27, 2026
b3b531f
feat(ipc): add interaction references and app context
domenkozar Aug 27, 2026
0040de5
preserve provider interactions in audit
domenkozar Aug 27, 2026
e4c6edd
feat(ipc): forward requested authorization duration
domenkozar Aug 27, 2026
3c9483e
simplify external provider discovery claims
domenkozar Aug 31, 2026
eb3df7a
fix(ipc): distinguish Windows directory ACL rights
mamoreau-devolutions Aug 31, 2026
d93c78c
test(ipc): preserve LF framing on Windows
mamoreau-devolutions Aug 31, 2026
0bdd1a6
feat(ipc): negotiate external provider credentials
domenkozar Sep 1, 2026
4c83fae
feat(ipc): add runtime protocol discovery
domenkozar Sep 3, 2026
d781ac8
ci: diagnose Windows CLI startup
domenkozar Sep 3, 2026
e1943ff
ci: dispatch Windows startup diagnostics
domenkozar Sep 3, 2026
6d0d55e
ci: remove startup diagnostic dispatch bridge
domenkozar Sep 3, 2026
d423530
fix(ipc): close callback and conformance gaps
domenkozar Sep 4, 2026
834c5af
Adapt IPC to main byte-native resolution and documentation
domenkozar Sep 11, 2026
0bc668a
feat(ipc): expose value-bound secret revisions
domenkozar Sep 13, 2026
cfbe88a
fix(ipc): reject incompatible reads and bound transport writes
domenkozar Sep 22, 2026
df01cdb
docs(ipc): fix credential brokerage anchor
domenkozar Sep 22, 2026
de8b09c
fix(ffi): remove duplicate test module declaration
domenkozar Sep 22, 2026
641eb55
fix(ipc): preserve request ordering and drain sessions gracefully
domenkozar Sep 22, 2026
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
45 changes: 45 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,12 @@ jobs:
uses: Swatinem/rust-cache@f0d9c3887740aee45f6153b24b3a6b815192ec16 # v2
with:
key: ${{ matrix.os }}
# libsecretspec-resolver takes yyjson from the system rather than vendoring
# it, and this job has no Nix to supply it. The Rust conformance package
# compiles the same C sources, so this has to precede every cargo step too.
- name: Install yyjson
shell: bash
run: bash scripts/install-yyjson.sh

# Formatting and linting are platform-independent; run them once on Linux.
# The PHP extension is checked in the SDK workflow, where PHP headers are
Expand All @@ -141,6 +147,28 @@ jobs:
run: cargo clippy --workspace --exclude secretspec-php-native
- name: Run tests
run: cargo test --workspace --exclude secretspec-php-native
# The C client is standalone C11, so it is built with its own build systems
# rather than the devenv shell: this job installs Rust directly and has no
# Nix, and Windows runners cannot run devenv at all.
- name: Install Meson and Ninja
shell: bash
# pipx rather than pip: the macOS runner's Python is externally managed
# (PEP 668), so a plain --user install is refused there.
run: |
pipx install meson
pipx install ninja
- name: Build standalone C IPC client with Meson
shell: bash
run: |
meson setup libsecretspec-resolver/build-meson libsecretspec-resolver
meson compile -C libsecretspec-resolver/build-meson
meson test -C libsecretspec-resolver/build-meson --print-errorlogs
- name: Build standalone C IPC client with CMake
shell: bash
run: |
cmake -S libsecretspec-resolver -B libsecretspec-resolver/build-cmake
cmake --build libsecretspec-resolver/build-cmake
ctest --test-dir libsecretspec-resolver/build-cmake --output-on-failure

# Each optional provider must compile on its own, not just in the default
# feature set. `cargo test` above cannot catch a missing feature gate: it
Expand Down Expand Up @@ -218,9 +246,26 @@ jobs:
run: rustup toolchain install stable --profile minimal
- name: Cache Rust builds
uses: Swatinem/rust-cache@f0d9c3887740aee45f6153b24b3a6b815192ec16 # v2
# This runner has no pkg-config, so the script also exports the explicit
# YYJSON_INCLUDE_DIR / YYJSON_LIB_DIR pair that the conformance build script
# falls back to.
- name: Install yyjson
shell: bash
run: bash scripts/install-yyjson.sh
- name: cargo test
# Exclude secretspec-php-native: it is an ext-php-rs extension that needs a
# PHP dev toolchain (php-config/headers) this bare runner does not have, and
# ext-php-rs rejects the runner's PHP version anyway. The PHP SDK is built
# and tested by sdks.yml / php-ext.yml instead.
run: cargo test --workspace --exclude secretspec-php-native
# The Rust conformance build compiles the Windows C launcher, but only the
# standalone suite executes its process/environment regressions. Keep this
# native CMake run on Windows so ACL-adjacent launch behavior cannot be
# "covered" solely by Linux and macOS tests of the POSIX implementation.
- name: Build and test standalone C IPC client
shell: pwsh
run: |
cmake -S libsecretspec-resolver -B libsecretspec-resolver/build-cmake
cmake --build libsecretspec-resolver/build-cmake --config Release
ctest --test-dir libsecretspec-resolver/build-cmake `
--build-config Release --output-on-failure
243 changes: 243 additions & 0 deletions .github/workflows/windows-startup.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
name: "Windows CLI startup diagnostics"

on:
workflow_dispatch:
inputs:
baseline_tag:
description: "Older release used as the startup baseline"
required: true
default: "v0.19.1"
type: string
comparison_tag:
description: "Newer release to compare with the baseline"
required: true
default: "v0.20.0"
type: string
iterations:
description: "Warm launches measured for each executable"
required: true
default: "50"
type: string

permissions:
contents: read

jobs:
diagnose:
name: Compare Windows CLI startup
runs-on: windows-latest
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BASELINE_TAG: ${{ inputs.baseline_tag || 'v0.19.1' }}
COMPARISON_TAG: ${{ inputs.comparison_tag || 'v0.20.0' }}
ITERATIONS: ${{ inputs.iterations || '50' }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Install Rust (pinned by rust-toolchain.toml)
run: rustup toolchain install --profile minimal

- name: Cache Rust build
uses: Swatinem/rust-cache@f0d9c3887740aee45f6153b24b3a6b815192ec16 # v2
with:
key: windows-startup

- name: Build the checked-out CLI
run: cargo build --package secretspec --bin secretspec --profile dist

- name: Download comparison releases
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$asset = "secretspec-x86_64-pc-windows-msvc.zip"
$releases = Join-Path $env:RUNNER_TEMP "secretspec-startup-releases"
$baseline = Join-Path $releases "baseline"
$comparison = Join-Path $releases "comparison"
New-Item -ItemType Directory -Force $baseline, $comparison | Out-Null

gh release download $env:BASELINE_TAG `
--repo cachix/secretspec `
--pattern $asset `
--dir $baseline
gh release download $env:COMPARISON_TAG `
--repo cachix/secretspec `
--pattern $asset `
--dir $comparison

Expand-Archive -LiteralPath (Join-Path $baseline $asset) `
-DestinationPath (Join-Path $baseline "expanded")
Expand-Archive -LiteralPath (Join-Path $comparison $asset) `
-DestinationPath (Join-Path $comparison "expanded")

- name: Benchmark process startup
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$iterations = 0
if (-not [int]::TryParse($env:ITERATIONS, [ref]$iterations) -or $iterations -lt 5 -or $iterations -gt 500) {
throw "iterations must be an integer between 5 and 500"
}

# Completion mode can inspect configuration and PATH. It is not part
# of ordinary startup and must not leak in from the runner image.
Remove-Item Env:SECRETSPEC_COMPLETE -ErrorAction SilentlyContinue

$runDirectory = Join-Path $env:RUNNER_TEMP "secretspec-startup-empty"
New-Item -ItemType Directory -Force $runDirectory | Out-Null
$releaseDirectory = Join-Path $env:RUNNER_TEMP "secretspec-startup-releases"

$candidates = @(
[pscustomobject]@{
Name = "baseline ($env:BASELINE_TAG)"
Path = Join-Path $releaseDirectory "baseline/expanded/secretspec.exe"
Samples = [System.Collections.Generic.List[double]]::new()
},
[pscustomobject]@{
Name = "comparison ($env:COMPARISON_TAG)"
Path = Join-Path $releaseDirectory "comparison/expanded/secretspec.exe"
Samples = [System.Collections.Generic.List[double]]::new()
},
[pscustomobject]@{
Name = "checked-out branch"
Path = Join-Path $env:GITHUB_WORKSPACE "target/dist/secretspec.exe"
Samples = [System.Collections.Generic.List[double]]::new()
}
)

# GitHub CLI downloads normally have no Mark-of-the-Web stream, but
# preserve and report it when the runner does attach one. In that
# case, benchmark an unblocked copy too so quarantine overhead is
# distinguishable from normal process startup.
foreach ($candidate in @($candidates[0..1])) {
$zone = Get-Item -LiteralPath $candidate.Path -Stream Zone.Identifier `
-ErrorAction SilentlyContinue
if ($null -ne $zone) {
$hashPrefix = (Get-FileHash `
-LiteralPath $candidate.Path `
-Algorithm SHA256).Hash.Substring(0, 12)
$unblockedPath = Join-Path $runDirectory `
"${hashPrefix}-unblocked.exe"
Copy-Item -LiteralPath $candidate.Path -Destination $unblockedPath
Unblock-File -LiteralPath $unblockedPath
$candidates += [pscustomobject]@{
Name = "$($candidate.Name), unblocked"
Path = $unblockedPath
Samples = [System.Collections.Generic.List[double]]::new()
}
}
}

function Measure-Launch([string]$executable) {
$startInfo = [System.Diagnostics.ProcessStartInfo]::new()
$startInfo.FileName = $executable
$startInfo.ArgumentList.Add("--version")
$startInfo.WorkingDirectory = $runDirectory
$startInfo.UseShellExecute = $false
$startInfo.CreateNoWindow = $true
$startInfo.RedirectStandardOutput = $true
$startInfo.RedirectStandardError = $true

$timer = [System.Diagnostics.Stopwatch]::StartNew()
$process = [System.Diagnostics.Process]::Start($startInfo)
$stdout = $process.StandardOutput.ReadToEnd()
$stderr = $process.StandardError.ReadToEnd()
$process.WaitForExit()
$timer.Stop()

if ($process.ExitCode -ne 0) {
throw "$executable exited with $($process.ExitCode): $stderr"
}
[pscustomobject]@{
Milliseconds = $timer.Elapsed.TotalMilliseconds
Version = $stdout.Trim()
}
}

foreach ($candidate in $candidates) {
if (-not (Test-Path -LiteralPath $candidate.Path -PathType Leaf)) {
throw "missing executable: $($candidate.Path)"
}
$first = Measure-Launch $candidate.Path
$candidate | Add-Member FirstMilliseconds $first.Milliseconds
$candidate | Add-Member Version $first.Version
}

# Rotate the order on every iteration so runner load over time does
# not consistently favor one executable.
for ($iteration = 0; $iteration -lt $iterations; $iteration++) {
for ($offset = 0; $offset -lt $candidates.Count; $offset++) {
$candidate = $candidates[($iteration + $offset) % $candidates.Count]
$measurement = Measure-Launch $candidate.Path
$candidate.Samples.Add($measurement.Milliseconds)
}
}

$results = foreach ($candidate in $candidates) {
$sorted = @($candidate.Samples | Sort-Object)
$medianIndex = [Math]::Floor(($sorted.Count - 1) / 2)
$p95Index = [Math]::Ceiling($sorted.Count * 0.95) - 1
$file = Get-Item -LiteralPath $candidate.Path
$signature = Get-AuthenticodeSignature -LiteralPath $candidate.Path
$zone = Get-Item -LiteralPath $candidate.Path -Stream Zone.Identifier `
-ErrorAction SilentlyContinue

[pscustomobject]@{
name = $candidate.Name
version = $candidate.Version
bytes = $file.Length
sha256 = (Get-FileHash -LiteralPath $candidate.Path -Algorithm SHA256).Hash.ToLowerInvariant()
signature = $signature.Status.ToString()
mark_of_the_web = $null -ne $zone
first_ms = [Math]::Round($candidate.FirstMilliseconds, 3)
median_ms = [Math]::Round($sorted[$medianIndex], 3)
p95_ms = [Math]::Round($sorted[$p95Index], 3)
minimum_ms = [Math]::Round($sorted[0], 3)
maximum_ms = [Math]::Round($sorted[-1], 3)
iterations = $iterations
}
}

$report = [pscustomobject]@{
schema_version = 1
generated_at = [DateTimeOffset]::UtcNow.ToString("o")
git_sha = $env:GITHUB_SHA
runner = [pscustomobject]@{
image_os = $env:ImageOS
image_version = $env:ImageVersion
os_version = [Environment]::OSVersion.VersionString
processor = $env:PROCESSOR_IDENTIFIER
}
baseline_tag = $env:BASELINE_TAG
comparison_tag = $env:COMPARISON_TAG
results = @($results)
}

$report | ConvertTo-Json -Depth 6 | Set-Content `
-LiteralPath windows-startup-results.json -Encoding utf8

@(
"## Windows CLI startup diagnostics"
""
"All launches used an absolute executable path, an empty working directory, and ``UseShellExecute = false``."
"Runner: ``$($report.runner.image_os) $($report.runner.image_version)``; OS: ``$($report.runner.os_version)``."
""
"| Executable | Version | Size (MiB) | First (ms) | Median (ms) | p95 (ms) | Signature | MOTW |"
"| --- | --- | ---: | ---: | ---: | ---: | --- | --- |"
) | Add-Content -LiteralPath $env:GITHUB_STEP_SUMMARY

foreach ($result in $results) {
$size = [Math]::Round($result.bytes / 1MB, 2)
"| $($result.name) | $($result.version) | $size | $($result.first_ms) | $($result.median_ms) | $($result.p95_ms) | $($result.signature) | $($result.mark_of_the_web) |" |
Add-Content -LiteralPath $env:GITHUB_STEP_SUMMARY
}

$results | Format-Table name, version, bytes, first_ms, median_ms, p95_ms

- name: Upload startup results
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: windows-startup-results
path: windows-startup-results.json
if-no-files-found: error
Loading
Loading