Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
496ed96
chore: update task status and refine acceptance criteria for detached…
MrMaxie Jun 16, 2026
f45870d
refactor: implement detached backend mode with explicit control for C…
MrMaxie Jun 16, 2026
22418ca
feat: add non-TUI CLI `cadderctl` for automation and inspection
MrMaxie Jun 16, 2026
9f8cf38
feat: add local MCP server for Cadder agents
MrMaxie Jun 16, 2026
a1859dc
docs: define OpenSpec architecture reset
MrMaxie Jul 7, 2026
1e19b02
feat: implement Cadder architecture reset slice
MrMaxie Jul 8, 2026
9829b5c
test: add daemon contract coverage and shim policy
MrMaxie Jul 8, 2026
f1d1afe
fix: keep managed shim runs on daemon path
MrMaxie Jul 8, 2026
2ace61a
fix: label read-only shim inspection output
MrMaxie Jul 8, 2026
a10c63a
test: cover real Caddy shim exclusion
MrMaxie Jul 8, 2026
5f4ca9e
feat: add authenticated Node runtime migration foundation
MrMaxie Oct 5, 2026
963c77d
fix: run native runtime gates in CI
MrMaxie Oct 5, 2026
198c1a8
fix: bootstrap the published Nub package in CI
MrMaxie Oct 5, 2026
e1c400e
fix: make runtime gates portable across native runners
MrMaxie Oct 5, 2026
8246d2e
test: budget for native Windows ACL checks
MrMaxie Oct 5, 2026
1670f35
test: prepare isolated Windows privilege acceptance gate
MrMaxie Oct 5, 2026
f850418
chore: merge master into Node runtime migration
MrMaxie Oct 5, 2026
93942af
docs: align Node migration plan with released product scope
MrMaxie Oct 5, 2026
76bec63
docs: record accepted Node contracts and migration evidence
MrMaxie Oct 8, 2026
0c1a5e6
feat: close Node RPC contracts and isolate installation runtimes
MrMaxie Oct 8, 2026
31b88e1
feat: add secure Caddy adapters and owned configuration lifecycle
MrMaxie Oct 8, 2026
81c120b
fix: preserve user ownership of native Windows runtime files
MrMaxie Oct 8, 2026
5b4daee
feat: implement Node product and candidate distributions
MrMaxie Oct 20, 2026
86eb714
test: make native migration checks portable
MrMaxie Oct 20, 2026
0180d68
test: expose native macOS failure diagnostics
MrMaxie Oct 20, 2026
5432a48
fix: preserve macOS socket owners and complete candidate assets
MrMaxie Oct 20, 2026
e3be920
test: verify one npm artifact and native terminal journeys
MrMaxie Oct 20, 2026
281d07d
fix: support cold Windows startup and explicit Unix owners
MrMaxie Oct 20, 2026
78700fc
test: exercise packaged TUI lifecycle in native terminals
MrMaxie Oct 20, 2026
e0a391b
test: use a portable native PTY for Unix product checks
MrMaxie Oct 20, 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
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@
*.md text eol=lf
*.yml text eol=lf
*.json text eol=lf
*.ts text eol=lf
*.tsx text eol=lf
125 changes: 125 additions & 0 deletions .github/workflows/node-runtime.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
name: Node runtime migration gate

on:
push:
branches:
- master
pull_request:
branches:
- master
workflow_dispatch:

permissions:
contents: read

jobs:
npm-artifact:
name: Pack the single npm candidate
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
filename: ${{ steps.pack.outputs.filename }}
steps:
- uses: actions/checkout@v6.0.3
- uses: actions/setup-node@v7.0.0
with:
node-version: 24.18.0
- run: npm install --global @nubjs/nub@0.7.5
- run: nub ci
- name: Pack once outside checkout
id: pack
run: |
node scripts/pack-node.ts > "$RUNNER_TEMP/cadder-npm.json"
node --input-type=module -e 'import fs from "node:fs"; import path from "node:path"; const report = JSON.parse(fs.readFileSync(path.join(process.env.RUNNER_TEMP, "cadder-npm.json"), "utf8")); fs.appendFileSync(process.env.GITHUB_OUTPUT, "archive=" + report.archive + "\nfilename=" + path.basename(report.archive) + "\n");'
- uses: actions/upload-artifact@v7.0.1
with:
name: cadder-npm-candidate
path: ${{ steps.pack.outputs.archive }}
if-no-files-found: error
- run: git diff --exit-code
runtime:
name: Node runtime (${{ matrix.os }})
needs: npm-artifact
runs-on: ${{ matrix.os }}
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
os: [windows-latest, ubuntu-latest, macos-15, macos-15-intel]
steps:
- uses: actions/checkout@v6.0.3
- uses: actions/setup-node@v7.0.0
with:
node-version: 24.18.0
- name: Bootstrap pinned repository manager
run: npm install --global @nubjs/nub@0.7.5
- name: Install exact locked dependencies
run: nub ci
- name: Check TypeScript
run: nub run typecheck
- name: Check lints
run: nub run lint
- name: Check formatting
run: nub run format:check
- name: Test runtime and coverage
env:
CADDER_COVERAGE_DIR: ${{ runner.temp }}/cadder-node-coverage
run: nub run coverage
- name: Pack native runtime fixtures outside checkout
id: fixtures
shell: bash
run: echo "stage=$(node scripts/prepare-runtime-gate.ts)" >> "$GITHUB_OUTPUT"
- name: Native compiled-JS runtime smoke without TS loader
run: node "${{ steps.fixtures.outputs.stage }}/runtime-smoke.mjs" "${{ steps.fixtures.outputs.stage }}/runtime-child.mjs"
- name: Native same-owner elevated daemon and different-account denial
if: runner.os != 'Windows'
run: node "${{ steps.fixtures.outputs.stage }}/runtime-privilege-smoke.mjs" "${{ steps.fixtures.outputs.stage }}/runtime-child.mjs" "${{ steps.fixtures.outputs.stage }}/runtime-denied-client.mjs"
- name: Prepare interactive Windows gate without launching Sandbox
if: runner.os == 'Windows'
run: node scripts/prepare-windows-runtime-sandbox.ts
- name: Prepare the separate, checksum-pinned upstream Caddy fixture
id: upstream
shell: bash
run: |
path="$(node scripts/prepare-caddy-gate.ts)"
echo "path=$path" >> "$GITHUB_OUTPUT"
- name: Allow the separate Linux fixture to bind web ports
if: runner.os == 'Linux'
run: sudo setcap cap_net_bind_service=+ep "${{ steps.upstream.outputs.path }}"
- name: Fetch the exact npm candidate
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v7.0.0
with:
name: cadder-npm-candidate
path: ${{ runner.temp }}/cadder-npm-candidate
- name: Verify packed npm and the shared real-Caddy catalog on the Node minimum
run: nub run verify:node "${{ steps.upstream.outputs.path }}" "${{ runner.temp }}/cadder-npm-candidate/${{ needs.npm-artifact.outputs.filename }}"
- name: Select the independently pinned SEA builder
uses: actions/setup-node@v7.0.0
with:
node-version: 26.12.0
- name: Build native SEA artifacts without publishing
id: sea
shell: bash
run: |
node scripts/pack-sea.ts > "$RUNNER_TEMP/cadder-sea.json"
node --input-type=module -e 'import fs from "node:fs"; import path from "node:path"; const report = JSON.parse(fs.readFileSync(path.join(process.env.RUNNER_TEMP, "cadder-sea.json"), "utf8")); fs.appendFileSync(process.env.GITHUB_OUTPUT, "archive=" + report.archive + "\nstage=" + report.stage + "\n");'
- name: Verify extracted SEA without Node/npm on product PATH
run: node scripts/verify-sea.ts "${{ steps.sea.outputs.archive }}"
- name: Verify SEA through the same real-Caddy catalog
run: node scripts/verify-product-journey.ts "${{ steps.sea.outputs.stage }}" "${{ steps.upstream.outputs.path }}"
- name: Retain exact native candidate artifacts
uses: actions/upload-artifact@v7.0.1
with:
name: cadder-node-candidate-${{ matrix.os }}
path: ${{ steps.sea.outputs.archive }}
if-no-files-found: error
- name: Verify no tracked build output
run: git diff --exit-code
- name: Upload coverage
if: always()
uses: actions/upload-artifact@v7.0.1
with:
name: node-coverage-${{ matrix.os }}
path: ${{ runner.temp }}/cadder-node-coverage
if-no-files-found: ignore
retention-days: 14
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Rust / Cargo
target/

# Node dependencies and transient test output
.astro/

# Build and release outputs
artifacts/
dist/
Expand Down
4 changes: 4 additions & 0 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"singleQuote": true,
"printWidth": 100
}
19 changes: 19 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,25 @@ mise run check

## Engineering Notes

### Node migration

`openspec/changes/reset-cadder-architecture/node-migration.md` defines the approved
2.0 target: one TypeScript/Node product with equal npm and Node SEA distributions.
The released Rust 1.0.5 workspace and its existing packaging remain the baseline
until both migration distribution gates pass. Do not restore superseded 0.8
crates or mix Rust and Node IPC.

Use `nub ci` and `nub run check` for the root TypeScript workspace. Sources and
runtime fixtures live in `src/`, `test/`, and `scripts/*.ts`; generated outputs
and coverage belong in system or runner temp outside the checkout. The Node
runtime gate supplements, rather than replaces, the existing Rust, docs, npm
and cargo-dist checks. Under the approved migration sequencing, Windows Sandbox
privilege acceptance runs on the completed product before G7 acceptance, Rust
removal or release. It does not block Caddy integration implementation; deferred
evidence must remain open rather than be recorded as passed.

### Released Rust baseline

- Cadder is cross-platform by default. OS-specific code must sit behind a small abstraction and keep Windows, Linux, and macOS behavior explicit.
- Runtime state is per user and rooted in `directories::ProjectDirs`, with `CADDER_RUNTIME_DIR` available for tests and custom deployments.
- IPC is versioned newline-delimited JSON over a per-user local socket via `interprocess`.
Expand Down
16 changes: 10 additions & 6 deletions crates/cadder-client/src/cli/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -432,16 +432,20 @@ mod tests {
LogEntry, LogEntryKind, LogSeverity, LogStreamIdentity, LogStreamStatus, OwnerProcessIdentity,
RegisteredDomain, RuntimeDiagnostic, RuntimeState, SourcePath,
};
use chrono::Utc;
use chrono::{TimeZone, Utc};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};

fn fixture_time() -> chrono::DateTime<Utc> {
Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).single().unwrap()
}

fn registration(
id: &str,
project: &str,
state: ActivationState,
domains: &[(&str, ActivationState, Option<&str>)],
) -> EntrypointRegistration {
let now = Utc::now();
let now = fixture_time();
let nonce = format!("{id}-nonce");
EntrypointRegistration {
registration_id: id.to_string(),
Expand Down Expand Up @@ -476,7 +480,7 @@ mod tests {
}

fn populated_snapshot() -> GuiStateSnapshot {
let now = Utc::now();
let now = fixture_time();
GuiStateSnapshot {
captured_at_utc: now,
registrations: vec![
Expand Down Expand Up @@ -548,7 +552,7 @@ mod tests {
#[test]
fn list_and_status_outputs_cover_empty_and_populated_snapshots() {
let empty = GuiStateSnapshot {
captured_at_utc: Utc::now(),
captured_at_utc: fixture_time(),
registrations: Vec::new(),
runtime: RuntimeState::idle(),
config: ConfigState::idle(),
Expand Down Expand Up @@ -651,7 +655,7 @@ mod tests {
#[test]
fn diagnostics_and_logs_cover_empty_and_detailed_reports() {
let empty = GuiStateSnapshot {
captured_at_utc: Utc::now(),
captured_at_utc: fixture_time(),
registrations: Vec::new(),
runtime: RuntimeState::idle(),
config: ConfigState::idle(),
Expand All @@ -671,7 +675,7 @@ mod tests {
stream_status: LogStreamStatus::Active,
entries: vec![LogEntry {
sequence_number: 7,
timestamp_utc: Utc::now(),
timestamp_utc: fixture_time(),
severity: LogSeverity::Warn,
stream,
attribution_kind: LogAttributionKind::Domain,
Expand Down
26 changes: 25 additions & 1 deletion crates/cadder-daemon/tests/fixtures/test_process.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::{
env,
fs::{self, OpenOptions},
io::{self, Write},
io::{self, Read, Write},
path::{Path, PathBuf},
process::{Command, ExitCode, Stdio},
thread,
Expand Down Expand Up @@ -68,6 +68,10 @@ fn fake_caddy(directory: &Path, mode: &str) -> io::Result<u8> {
let arguments = env::args().skip(1).collect::<Vec<_>>();
append_line(directory.join("fake-caddy.log"), &arguments.join(" "))?;

if mode.starts_with("shim-passthrough-") {
return shim_passthrough(directory, mode, &arguments);
}

match arguments.first().map(String::as_str) {
Some("adapt") => adapt(mode),
Some("reload") => reload(directory, mode),
Expand All @@ -77,6 +81,26 @@ fn fake_caddy(directory: &Path, mode: &str) -> io::Result<u8> {
}
}

fn shim_passthrough(directory: &Path, mode: &str, arguments: &[String]) -> io::Result<u8> {
let mut stdin = Vec::new();
io::stdin().read_to_end(&mut stdin)?;
let record = serde_json::json!({
"args": arguments,
"stdin_bytes": stdin,
});
fs::write(
directory.join("shim-passthrough.json"),
serde_json::to_vec(&record).map_err(io::Error::other)?,
)?;
println!("FAKE_CADDY_PASSTHROUGH_STDOUT");
eprintln!("FAKE_CADDY_PASSTHROUGH_STDERR");
Ok(if mode == "shim-passthrough-failure" {
23
} else {
0
})
}

fn adapt(mode: &str) -> io::Result<u8> {
match mode {
"adapter-fail-adapt" | "project-adapt-fail" => {
Expand Down
Loading
Loading