Merge v3-t103: seed /srv/www as one folder userland/web/www, whole-folder copy, Nix check for GET / (T103, D70)

This commit is contained in:
2026-11-12 20:16:27 +00:00
12 changed files with 54 additions and 18 deletions
+4 -2
View File
@@ -53,6 +53,8 @@ Coreutils, diffutils, procps, and brush each occupy one real binary in `bin/`.
Their command names are relative symlinks to those binaries.
The world copier preserves these symlinks and dangling links in the source tree.
A fresh development world needs no manual source copy.
Both bundle builders copy `userland/web/www/` whole into `/srv/www/`, with the original filenames and the Marked license.
The server crate, service unit, and test scripts stay outside that folder.
The bundle includes `/bin/web.wasm` and an enabled `/etc/systemd/system/web.service`.
World init starts that service on `0.0.0.0:8080`.
To change its address, edit `ExecStart` to add `--listen ADDR`, then run `systemctl restart web` inside the world.
@@ -166,8 +168,8 @@ node userland/web/browser-check.mjs
The script uses agent-browser and a CDP virtual authenticator. It registers a passkey and approves an item creation request.
Set `AGENT_BROWSER_EXECUTABLE_PATH` if Chrome needs an explicit path.
Sources: [`host API`](../host/src/api/README.md), [`credential views`](../userland/web/credentials-view.js),
[`edits.rs`](../host/src/cred/edits.rs), and [`browser cryptography`](../userland/web/credentials.js).
Sources: [`host API`](../host/src/api/README.md), [`credential views`](../userland/web/www/credentials-view.js),
[`edits.rs`](../host/src/cred/edits.rs), and [`browser cryptography`](../userland/web/www/credentials.js).
## Models
+8 -1
View File
@@ -137,7 +137,7 @@
cp -R ${./config/roster} $out/etc/sausage/roster
cp ${./userland/web/web.service} $out/etc/systemd/system/web.service
touch $out/etc/systemd/system/multi-user.target.wants/web.service
cp ${./userland/web/index.html} ${./userland/web/credentials.js} ${./userland/web/credentials-view.js} $out/srv/www/
cp -R ${./userland/web/www}/. $out/srv/www/
'';
sausageUnwrapped = craneLib.buildPackage (hostArgs // {
cargoArtifacts = hostArtifacts;
@@ -200,6 +200,13 @@
test -f "$TMPDIR/world/world/etc/sausage/roster/sausage.md"
touch $out
'';
checks.web = pkgs.runCommand "sausage-web-seed" {
nativeBuildInputs = [ pkgs.nushell pkgs.curl ];
} ''
export SAUSAGE_CACHE="$TMPDIR/sausage-cache"
nu ${./userland/web/seed-check.nu} ${sausage}/bin/sausage ${bundle}
touch $out
'';
apps.default = { type = "app"; program = "${sausage}/bin/sausage"; };
devShells.default = pkgs.mkShell (v8Env // wasiCcEnv // moldEnv // {
packages = [ rust pkgs.wasmtime pkgs.openssl pkgs.pkg-config pkgs.go pkgs.cmake pkgs.llvmPackages_21.lld ] ++ builderTools ++ mold;
+1 -1
View File
@@ -309,7 +309,7 @@ async fn approvals_registration_and_revocation() -> Result<()> {
async fn encrypt(_api: &Api, public_key: Value) -> Result<Envelope> {
use deno_core::{JsRuntime, RuntimeOptions};
let script = include_str!("../../../userland/web/credentials.js");
let script = include_str!("../../../userland/web/www/credentials.js");
let mut runtime = JsRuntime::new(RuntimeOptions {
extensions: vec![
deno_webidl::deno_webidl::init(),
+2 -6
View File
@@ -42,7 +42,7 @@ fn empty_rule_summary() -> anyhow::Result<()> {
#[test]
fn page_rendering() -> anyhow::Result<()> {
let source = Path::new(env!("CARGO_MANIFEST_DIR")).join("../userland/web");
let page = std::fs::read_to_string(source.join("index.html"))?;
let page = std::fs::read_to_string(source.join("www/index.html"))?;
let script = page
.split_once("<script>")
.unwrap()
@@ -120,11 +120,7 @@ async fn seeded_chat_case(open_connection: bool) -> anyhow::Result<()> {
&bundle.path().join("src/sausage/userland/web"),
)?;
let www = bundle.path().join("srv/www");
std::fs::create_dir_all(&www)?;
std::fs::copy(
source.join("userland/web/index.html"),
www.join("index.html"),
)?;
crate::common::copy_tree(&source.join("userland/web/www"), &www)?;
let dir = tempfile::tempdir()?;
drop(World::open_with_userland(dir.path(), Some(bundle.path()))?);
let port = std::net::TcpListener::bind("127.0.0.1:0")?
+1 -1
View File
@@ -115,7 +115,7 @@ impl Browser {
item["fields"][0]["field_id"].as_str().unwrap()
);
let public = self.json("GET", "/cred/entry/key", Value::Null)?;
let script = include_str!("../../../userland/web/credentials.js");
let script = include_str!("../../../userland/web/www/credentials.js");
let mut runtime = deno_core::JsRuntime::new(deno_core::RuntimeOptions {
extensions: vec![
deno_webidl::deno_webidl::init(),
+2 -6
View File
@@ -40,12 +40,8 @@ def main [destination?: path] {
mkdir ($units | path join multi-user.target.wants)
let www = ($destination | path join srv www)
mkdir $www
^cp --remove-destination ($here | path join web index.html) ($www | path join index.html)
if $env.LAST_EXIT_CODE != 0 { error make {msg: 'web page copy failed'} }
for file in [credentials.js credentials-view.js] {
^cp --remove-destination ($here | path join web $file) ($www | path join $file)
if $env.LAST_EXIT_CODE != 0 { error make {msg: 'credential UI copy failed'} }
}
^cp -R --remove-destination ($here | path join web www .) $www
if $env.LAST_EXIT_CODE != 0 { error make {msg: 'web folder copy failed'} }
^cp --remove-destination ($here | path join web web.service) ($units | path join web.service)
if $env.LAST_EXIT_CODE != 0 { error make {msg: 'web unit copy failed'} }
'' | save --force ($units | path join multi-user.target.wants web.service)
+1 -1
View File
@@ -67,7 +67,7 @@ assert.equal(run("requestSummary('Give access to 1 credential items for 1 hour.'
assert.equal(run("requestSummary('Give access to 11 credential items for 1 hour.')"),'Give access to 11 credential items for 1 hour.');
context.conversations=[{id:1,agent:null},{id:24,agent:null},{id:25,agent:{}},{id:14,agent:{model:{provider:'faux'}}}];
assert.equal(run('visibleConversations(conversations).map(c=>c.id).join()'),'1,14');
vm.runInContext(fs.readFileSync(__dirname+'/credentials-view.js','utf8').split('const credentialView=')[0],context);
vm.runInContext(fs.readFileSync(__dirname+'/www/credentials-view.js','utf8').split('const credentialView=')[0],context);
const config={id:'one',name:'Example <item>',rules:[{scheme:'https',hosts:['api.example.com'],port:443,path_prefixes:['/v1'],methods:['GET']}],fields:[{name:'token',public:false,field_type:'text'}],transform_js:'present'};
context.review={summary:'Create item',subject:{kind:'program',id:'/bin/curl'},payload:{create:config}};
context.items=[config];
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env nu
# Check the packaged seed through its web service without configured providers.
def main [host: path, bundle: path] {
let world = ($env.TMPDIR | path join web-world)
^$host init $world
if $env.LAST_EXIT_CODE != 0 { error make {msg: 'seed init failed'} }
'providers: {}' | save --force ($world | path join world etc sausage config.yaml)
let server = (job spawn {
'' | ^$host run $world out> server.stdout err> server.stderr
})
try {
let deadline = (date now) + 60sec
loop {
let response = (^curl --silent --show-error --noproxy '*' --max-time 2 --output whoami.json --write-out '%{http_code}' http://127.0.0.1:8080/api/whoami | complete)
if $response.exit_code == 0 and $response.stdout == '200' { break }
if (date now) >= $deadline { error make {msg: $'GET /api/whoami did not return 200: ($response)'} }
}
let response = (^curl --silent --show-error --noproxy '*' --max-time 5 --output page.html --write-out '%{http_code}' http://127.0.0.1:8080/ | complete)
if $response.exit_code != 0 or $response.stdout != '200' {
error make {msg: $'GET / expected 200, received ($response.stdout): ($response.stderr)'}
}
if not (open --raw page.html | str contains 'Sausage chat') { error make {msg: 'GET / did not return the chat page'} }
let paths = (^find $bundle -printf '%P\n' | complete)
if $paths.exit_code != 0 { error make {msg: $paths.stderr} }
if ($paths.stdout | lines | any {|path| $path =~ '(^|/)[0-9a-z]{32}-' }) {
error make {msg: 'a bundle path has a Nix store hash prefix'}
}
} catch {|err|
job kill $server
print --stderr (open --raw server.stderr)
error make {msg: $err.msg}
}
job kill $server
print 'PASS packaged seed: GET /, GET /api/whoami, and unprefixed paths'
}