Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,10 @@ captures/
/*.avif
/*.mp4
/*.png
.personal/

# Orion-OS local working context (memory bank, agent config) — never committed.
.agents/

# macOS Finder metadata
.DS_Store
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ irm https://jcode.sh/install.ps1 | iex
Need Homebrew, source builds, provider setup, or want an agent to set it up for you?
[Jump to detailed installation](#detailed-installation).

Installs and updates verify a SHA-256 checksum over HTTPS. For the full
supply-chain trust model (what is and isn't verified, and how to install more
cautiously), see [`SECURITY.md`](SECURITY.md).

---


Expand Down Expand Up @@ -306,6 +310,10 @@ Jcode is left-aligned by default. You can switch to centered mode with the `Alt+

To disable emoji globally in TUI and CLI output, set `emoji = false` under `[display]` in `~/.jcode/config.toml`, or launch with `JCODE_NO_EMOJI=1`. Jcode replaces emoji with compact ASCII markers while preserving other Unicode text.

### Accessibility

jcode is keyboard-driven, honors `NO_COLOR`/`JCODE_NO_COLOR`, encodes status with glyphs (not color alone), and measures theme contrast. Because it is a terminal UI, it has no dedicated screen-reader announcement channel yet. See [`docs/ACCESSIBILITY.md`](docs/ACCESSIBILITY.md) for an honest account of what works today, current limitations, and recommendations for screen-reader users.

---

## Swarm
Expand Down
72 changes: 72 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Security

This document covers jcode's software supply-chain trust model for
installation and updates (audit finding SEC-07), and where to report issues.

## Reporting a vulnerability

Please report security issues privately to the maintainer rather than opening a
public issue. Include repro steps and the affected version/commit.

## Install & update trust model (SEC-07)

### What is verified today

- **Checksum integrity.** The install script and the in-app updater download a
`SHA256SUMS` file and verify the downloaded binary's SHA-256 against it
(`scripts/install.sh`, `crates/jcode-update-core::verify_asset_checksum_text`).
The installer tries multiple checksum sources (the release metadata host and
the GitHub release assets) before trusting one.
- **Transport.** All downloads are over HTTPS/TLS.

This reliably prevents **accidental corruption** (partial downloads, mirror
rot, CDN glitches).

### What is NOT verified — and the residual risk

- **No independent cryptographic signature.** The `SHA256SUMS` file and the
binary share the same trust root (the release host / repository). An attacker
who compromises that origin can serve a malicious binary *and* a matching
`SHA256SUMS`, and checksum verification will pass. Checksums prove integrity,
not authenticity.
- **`curl | bash` install is trust-on-first-use.** `curl -fsSL
https://jcode.sh/install | bash` (and the PowerShell equivalent) pipes remote
code into a shell with the user's privileges. This is industry-standard for
CLI tools but means a compromise of the install host serves code directly.
- **Prebuilt binaries are not code-signed/notarized** at time of writing, so the
OS cannot independently attest their origin.

### Recommendations for cautious users

- Prefer building from source (`cargo build --release`) if you want to avoid the
`curl | bash` trust-on-first-use step.
- Or download the release archive and the `SHA256SUMS` from the GitHub release
page, inspect the installer script before running it, and verify the checksum
by hand.
- Pin to a specific released version rather than always taking latest.

### Hardening roadmap (tracked, not yet implemented)

These are the concrete steps to close the authenticity gap. They are recorded
here so the trust model is honest and the work is visible; they intentionally
require a maintainer decision on signing-key custody and are out of scope for
the change that added this document.

1. **Detached signatures over `SHA256SUMS`.** Sign the sums file with a
long-lived key (minisign/signify or cosign/Sigstore) whose public key is
published out-of-band (README, website, and a pinned repo file). Verify the
signature in `jcode-update-core` and `scripts/install.sh` before trusting any
checksum. `jcode-update-core` already isolates checksum parsing/verification,
so a signature-verification step slots in ahead of it behind an opt-in
configured public key (no behavior change until a key ships).
2. **Multi-channel checksum/signature publication.** Publish the sums (and their
signature) on at least two independent roots (e.g. GitHub releases + a signed
git tag) so a single-origin compromise is insufficient.
3. **Platform code signing / notarization.** Sign+notarize macOS binaries and
sign Windows binaries so the OS attests origin; verify update payloads
against those signatures where the platform supports it.
4. **Pin the installer artifact hash inside the install script** so the script
and the artifact it fetches are bound together.

Until (1)-(3) land, treat installation and updates as trust-on-first-use rooted
in the release host's integrity.
82 changes: 79 additions & 3 deletions crates/jcode-app-core/src/network_retry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,59 @@ pub fn wait_plan() -> NetworkWaitPlan {
}
}

pub async fn wait_until_probably_online() {
/// Default ceiling for a single reconnect wait. Long enough to ride out a VPN
/// flap or a sleeping laptop, short enough that a permanently offline state
/// (broken VPN profile, captive portal that never satisfies the probe) cannot
/// wedge a caller forever (REL-01).
pub const DEFAULT_RECONNECT_CEILING: Duration = Duration::from_secs(300);

/// Outcome of a bounded reconnect wait.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ReconnectOutcome {
/// Connectivity was observed; the caller should retry its request.
Online,
/// The ceiling elapsed while still offline; the caller must surface this
/// and stop rather than block forever.
GaveUp { waited: Duration },
}

impl ReconnectOutcome {
pub fn is_online(self) -> bool {
matches!(self, ReconnectOutcome::Online)
}
}

/// Wait until connectivity is probably restored, bounded by [`DEFAULT_RECONNECT_CEILING`].
///
/// REL-01: previously this looped forever with exponential backoff and no
/// ceiling, so a permanent offline state wedged the caller with no escape. It
/// now returns [`ReconnectOutcome::GaveUp`] once the ceiling elapses so callers
/// can fail with a visible diagnostic instead of hanging.
pub async fn wait_until_probably_online() -> ReconnectOutcome {
wait_until_probably_online_bounded(DEFAULT_RECONNECT_CEILING).await
}

/// Bounded reconnect wait with an explicit total-time ceiling.
///
/// Polls with exponential backoff (capped at 30s per interval) until either
/// connectivity is observed ([`ReconnectOutcome::Online`]) or `max_total`
/// elapses ([`ReconnectOutcome::GaveUp`]). A zero or negative budget still makes
/// at least one connectivity probe so a transient blip is caught cheaply.
pub async fn wait_until_probably_online_bounded(max_total: Duration) -> ReconnectOutcome {
let start = std::time::Instant::now();
let mut delay = Duration::from_secs(1);
loop {
if probe_connectivity().await {
return;
return ReconnectOutcome::Online;
}
wait_for_platform_change_or_delay(delay).await;
let elapsed = start.elapsed();
if elapsed >= max_total {
return ReconnectOutcome::GaveUp { waited: elapsed };
}
// Never sleep past the remaining budget, so we return close to the
// ceiling rather than overshooting by a full backoff interval.
let remaining = max_total - elapsed;
wait_for_platform_change_or_delay(delay.min(remaining)).await;
delay = (delay * 2).min(Duration::from_secs(30));
}
}
Expand Down Expand Up @@ -183,6 +229,36 @@ async fn wait_for_command_output(command: &str, args: &[&str]) {
mod tests {
use super::*;

/// REL-01: a bounded wait must return within roughly its ceiling and never
/// hang, whatever the network state. With a tiny budget it resolves fast;
/// if offline it reports `GaveUp` rather than looping forever.
#[tokio::test]
async fn bounded_wait_respects_its_ceiling() {
let ceiling = Duration::from_millis(200);
let start = std::time::Instant::now();
let outcome = wait_until_probably_online_bounded(ceiling).await;
let elapsed = start.elapsed();

// Must not overshoot the ceiling by more than one probe timeout (5s)
// plus scheduling slack — the key property is that it terminates.
assert!(
elapsed < ceiling + Duration::from_secs(8),
"bounded wait ran {elapsed:?}, far past its {ceiling:?} ceiling"
);
// Whatever the CI network state, the outcome must be one of the two
// terminal states (i.e. the function returned at all).
match outcome {
ReconnectOutcome::Online => assert!(outcome.is_online()),
ReconnectOutcome::GaveUp { waited } => {
assert!(!outcome.is_online());
assert!(
waited >= ceiling,
"GaveUp waited {waited:?} < ceiling {ceiling:?}"
);
}
}
}

#[test]
fn classifies_common_network_errors() {
assert!(classify_message("connection reset by peer").is_some());
Expand Down
5 changes: 2 additions & 3 deletions crates/jcode-app-core/src/tool/bash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -835,9 +835,8 @@ fn default_true() -> bool {
true
}

#[path = "bash_destructive_gate.rs"]
mod destructive_gate;
use destructive_gate::destructive_command_refusal;
use super::destructive_gate;
use super::destructive_gate::destructive_command_refusal;
#[async_trait]
impl Tool for BashTool {
fn name(&self) -> &str {
Expand Down
88 changes: 0 additions & 88 deletions crates/jcode-app-core/src/tool/bash_destructive_gate.rs

This file was deleted.

Loading