Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,16 @@ jobs:
win-*) cp "$BIN" "weir-${{ github.ref_name }}-${{ matrix.rid }}.exe" ;;
*) cp "$BIN" "weir-${{ github.ref_name }}-${{ matrix.rid }}" ;;
esac
# the ARTIFACT, not just the source tree, must uphold the output-hygiene
# guarantees — every strix retest found a fix "correct in source, still
# present in the shipped binary". The binary self-tests before it can ship.
- name: security smoke — the packaged binary upholds the hygiene guarantees
run: |
case "${{ matrix.rid }}" in
win-*) BIN=src/Weir/bin/Release/net10.0/${{ matrix.rid }}/publish/Weir.exe ;;
*) BIN=src/Weir/bin/Release/net10.0/${{ matrix.rid }}/publish/Weir ;;
esac
"$BIN" ci/release-smoke.weir --bin "$BIN"
# signed build provenance [D:install-checksum-scope]: the installer's
# embedded checksum is the primary trust anchor; this adds an
# independent, OIDC-signed record that THIS repo's Actions built the
Expand Down
57 changes: 57 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,62 @@
# Changelog

## v0.0.50

### Added

- **`exec`: process replacement.** `cmd | exec` replaces the running weir
process with the command (POSIX `execve`; Windows spawns, waits, and
exits with the child's code). weir keeps its pid, so as a container
entrypoint the application receives signals directly — no forwarding or
reaping layer. It is a reifier like `complete`/`orFail`, diverging like
`fail`/`exit` (never returns), so it is a legal bare statement; it takes
a literal or dynamic (`^$cmd`) head and an env-sigil overlay
(`$e(cmd | exec)`), but refuses a piped stdin — there is no parent left
to feed the replacement.

- **`Path.home` and the XDG directory trio (`Path.configHome`,
`Path.stateHome`, `Path.cacheHome`).** The typed stand-in for `~`/`$HOME`,
which never expand in argv — build a path with an interpolation instead:
`cat $"{Path.home ()}/.bashrc"`, `cat $"{Path.stateHome ()}/weir/history"`.
Each is a pure `unit -> string` query with platform-native output:
`configHome`/`stateHome`/`cacheHome` resolve `%APPDATA%`/`%LOCALAPPDATA%`
on Windows and `$XDG_CONFIG_HOME`/`$XDG_STATE_HOME`/`$XDG_CACHE_HOME`
(falling back to `~/.config`, `~/.local/state`, `~/.cache`) on POSIX —
the same resolution the REPL uses for its own history file.

- **`#history` REPL directive.** Shows the session's history with the
**file path in the header** (a quick way to find where history lives,
since `~` never expands): bare `#history` dumps every entry numbered,
`#history N` shows the last N. Entries render one per line, matching the
history search's display.

- **A checker warning for the `Path.newTempDir` cleanup footgun.** A
`Path.newTempDir ()` binding that is later `Dir.delete`/`Dir.deleteAll`'d
in the same scope now draws an advisory warning pointing at `within tmp`,
which cleans up on scope exit *and* on Ctrl+C/kill (a manual delete
misses the signalled case). `newTempDir` remains the right tool for a
directory that must outlive its scope, so a binding with no in-scope
delete stays silent. Warning severity — `check` still exits 0.

### Fixed

- **A top-level `if … then … else …` block now parses.** The block form at
column 0 (`if c then` + an indented body, then a dedented `else`/`elif`)
was rejected as a stray `else` keyword — the assembler treated the
dedented `else` as a new statement. A col-0 `else`/`elif` now continues
its `if`, the same way a dedented `|`/`until`/`always` already did. (The
indented form inside a function body always worked.)

- **`#infer` no longer mis-drafts a heterogeneous object as a homogeneous
map.** When sibling objects in a sample array carried different keys whose
values only *coincidentally* agreed in an early pair (e.g. a Kubernetes
`securityContext` with a `bool` field, then an `int` field two elements
later), the pairwise merge committed to `seq<string * bool>` and a later
`from json` rejected the int (`expected bool, got Number`). The open-map
verdict is now decided over the *whole* set of siblings, so an object with
mixed value types stays a typed record; genuinely uniform data-keyed
objects (a ConfigMap's `data`) are still drafted as an open mapping.

## v0.0.49

### Fixed
Expand Down
5 changes: 4 additions & 1 deletion ci/commit-area.weir
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,10 @@ let areaOfOther p =
"site"
elif Str.startsWith "ci/" p || Str.startsWith ".github/" p || Str.startsWith "tools/" p || Str.startsWith "tests/pty/" p then
"ci"
elif Str.startsWith "publish." p || Str.startsWith "install." p || p == "Dockerfile" || p == "weir.slnx" || p == ".gitignore" || p == ".dockerignore" || p == ".gitattributes" then
elif Str.startsWith "publish." p || Str.startsWith "install." p || p == "Dockerfile" || p == "weir.slnx" || p == ".gitignore" || p == ".dockerignore" || p == ".gitattributes" || Str.endsWith ".fsproj" p then
// a .fsproj with no accompanying .fs change is BUILD config (it
// rides the src it configures via areaOfSrc when both move; alone,
// it is ci, never the docs fallback) [D:commit-areas]
"ci"
elif Str.startsWith "tests/" p then
"tests"
Expand Down
40 changes: 40 additions & 0 deletions ci/e2e.sh
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,46 @@ expect "POSIX one-liner via the external shell" '["a"; "b"]' "$out"
out=$($BIN -e 'sh -c "exit 7" | complete |> _.exitCode')
expect "sh lines can complete now (old builtin boundary gone)" "7 : int" "$out"

# ---- exec: process replacement [D:exec] -----------------------------
# exec REPLACES the runner (execve), so the command's own stdout AND exit
# code ARE the script's — there is no weir layer left to reify through.
# Out-of-process by nature: -e / a script drives it and the child's bytes
# and status are observed directly. POSIX-only (execve; the coreutils
# heads have no Windows shadow — the Windows spawn-wait-exit path is the
# hand-run item, like the other sh/coreutils cells).
if [ "$IS_WINDOWS" = "0" ]; then
out=$($BIN -e 'echo exec-marker | exec')
expect "exec replaces the process with the command" "exec-marker" "$out"

execdir=$(mkweirtmp)
cat > "$execdir/exec.weir" <<'WEOF'
sh -c "exit 7" | exec
print "unreached"
WEOF
# exec replaces with `sh -c "exit 7"`, so the runner exits 7 BY DESIGN —
# capture it without tripping the battery's set -e
code=0
$BIN "$execdir/exec.weir" || code=$?
[ "$code" = "7" ] || fail "exec must exit with the replacement's code (got $code)"
out=$($BIN "$execdir/exec.weir" 2>&1 || true)
echo "$out" | grep -qF "unreached" && fail "no statement runs after exec — the image is gone"

# the env overlay reaches the replacement (libc setenv, since execvp
# reads the C environ, not .NET's managed copy)
cat > "$execdir/execenv.weir" <<'WEOF'
let e = [Env.pair "EXECENV" "reached"]
$e(printenv EXECENV | exec)
WEOF
out=$($BIN "$execdir/execenv.weir")
expect "exec's env overlay reaches the replacement" "reached" "$out"

# a value pipe into exec is refused at parse (no parent to feed stdin);
# `check` exits nonzero on that error BY DESIGN — do not let set -e abort
printf '["a"] | grep a | exec\n' > "$execdir/execbad.weir"
out=$($BIN check "$execdir/execbad.weir" 2>&1 || true)
echo "$out" | grep -qF "cannot take a piped stdin" || fail "value-headed exec must refuse at check"
fi

# a 2-param generic union checks + evals through the binary (was the
# prelude-Result pin; Result removed [D:no-result], the fixture is now a
# locally-declared Either)
Expand Down
54 changes: 54 additions & 0 deletions ci/release-smoke.weir
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env weir
// Release SECURITY SMOKE [D:release-artifact-smoke]: the strix retests
// found every credential/output-hygiene fix "correct in source, still
// present in the SHIPPED binary" — a release-publishing gap, not a code
// one. A guarantee that lives only in an unreleased tree protects no user.
// This gate runs the promises against a BINARY (the packaged artifact), so
// a build that regressed one cannot reach "released".
//
// weir ci/release-smoke.weir --bin <path-to-weir>
//
// The probes use the transport-error path (a bogus host — no server) and
// file-run rendering (no tty), so the gate is self-contained and portable.

type Cli = { bin: string }

let cli = Args.load Cli
let bin = cli.bin

// run <bin> on a one-off script; return the whole diagnostic stream and exit
let probe program =
within tmp d
let path = $"{d}/probe.weir"
program |> File.write path
let r = ^$bin $path | complete
let text = Seq.append r.stdout r.stderr |> Str.join "\n"
(text, r.exitCode)

// 1. HTTP URL userinfo is REDACTED in error output (CWE-532)
let out1, _ = probe [ @"let _r = Http.fetch ""http://alice:hunter2@nonexistent.invalid/x""" ]
if Str.contains "alice:hunter2" out1 then
fail $"REDACTION: URL userinfo printed verbatim in the error — {out1}"
if not (Str.contains "***" out1) then
fail $"REDACTION: no *** mask in the transport error — {out1}"
print "ok: HTTP userinfo redacted (user:pass -> ***)"

// 2. parse/decode error output is BOUNDED, not proportional to input (CWE-1286)
let out2, _ =
probe [ @"let big = Str.replicate 500000 ""!"""; "let _b = Bytes.fromBase64 big" ]
let n2 = Str.length out2
if n2 > 2000 then
fail $"BOUNDED-OUTPUT: a 500k invalid input yielded {n2} bytes of error (unbounded echo)"
print $"ok: parse/decode error bounded ({n2} bytes for a 500k input)"

// 3. a deeply-nested value does NOT crash the process (CWE-674, file-run render)
let _, code3 =
probe
[ "type N = { d: int; next: Option<N> }"
"let deep = [1..200000] |> Seq.fold (fun acc i -> N { d = i; next = Some acc }) (N { d = 0; next = None })"
"print (Str.length (show deep))" ]
if code3 <> 0 then
fail $"DEEP-VALUE: a 200k-deep value crashed the binary (exit {code3} — likely SIGSEGV/SIGABRT)"
print "ok: deep value renders without crashing (exit 0)"

print $"release-smoke: all security guarantees hold in {bin}"
Loading
Loading