From e5cfe3994f935273f3ff35925c512347b1546d06 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 12:10:29 +0000 Subject: [PATCH 1/5] feat(ipc): command table and async bridge; register coprocessor (pilot) New GTK-free lib module `ipc` adapts panel-backend handlers to Gossamer's handler shape. main.rs registers every table entry with `command_async`, so calls run on a worker thread instead of the GTK main thread. Conventions: - Arguments are read by parameter name, snake_case key first, then the camelCase form. Tauri renamed camelCase keys for us; the Gossamer bridge does not, and the old frontend sent both (e.g. `providerId`). - A missing argument reads as null: Option parameters get None, and a required one fails with "missing argument `x`" before the handler runs. - Struct parameters take the named key if present, otherwise the whole payload. The old frontend sent both shapes. - Responses use main.rs's envelope, {"ok", "result"|"error"}. A result that isn't JSON is kept as a string rather than becoming null. - async fn handlers run on one shared multi-thread Tokio runtime via `ipc::block_on`. Sync handlers are called directly, because several use reqwest::blocking, which must not run inside a runtime. Pilot: coprocessor's 10 handlers (F2 in docs/TECHNICAL_DEBT.adoc). Tests (lib, 8 new): argument lookup, struct shapes, the envelope, unique names, no clash with main.rs's own commands, and every entry with a required argument rejecting `{}` before its body runs. Coprocessor commands are also called through the table. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SJtjqoZaSQo7uBwvwVMA9G --- src-gossamer/src/ipc.rs | 336 +++++++++++++++++++++++++++++++++++++++ src-gossamer/src/lib.rs | 11 +- src-gossamer/src/main.rs | 12 ++ 3 files changed, 354 insertions(+), 5 deletions(-) create mode 100644 src-gossamer/src/ipc.rs diff --git a/src-gossamer/src/ipc.rs b/src-gossamer/src/ipc.rs new file mode 100644 index 00000000..a6c85b39 --- /dev/null +++ b/src-gossamer/src/ipc.rs @@ -0,0 +1,336 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) + +//! IPC command table for the panel-backend modules. +//! +//! Each entry adapts one backend function to the Gossamer IPC handler shape, +//! `Fn(serde_json::Value) -> Result`. The table is +//! GTK-free, so `cargo test --lib` checks every entry; `main.rs` registers it +//! with `App::command_async`, which runs each call on a worker thread instead +//! of the GTK main thread. +//! +//! Conventions shared by every entry: +//! - Arguments are read by parameter name: the snake_case key first, then its +//! camelCase form. Tauri renamed camelCase keys to snake_case for us; the +//! Gossamer bridge does not, and the old frontend sent both. +//! - A missing argument reads as JSON `null`. `Option` parameters get `None`; +//! a required one fails with "missing argument `x`" before the handler runs. +//! - Responses use the envelope the rest of `main.rs` uses: +//! `{"ok": true, "result": …}` or `{"ok": false, "error": "…"}`. A result +//! string that is valid JSON is embedded as JSON, anything else as a string. +//! - `async fn` handlers run to completion on one shared Tokio runtime, so +//! tasks they spawn (e.g. via `compat::spawn_async`) keep running after the +//! call returns. + +use serde::de::DeserializeOwned; +use serde_json::{json, Value}; +use std::future::Future; +use std::sync::OnceLock; + +/// A Gossamer IPC handler. +pub type Handler = Box Result + Send + 'static>; + +/// One IPC command. +pub struct Command { + /// Command name, as the frontend passes it to `invoke`. + pub name: &'static str, + /// Arguments the handler cannot run without: every parameter that is not + /// an `Option` and is not supplied by the backend itself. + pub required: &'static [&'static str], + /// The handler. It always returns `Ok`; failures are in the envelope. + pub handler: Handler, +} + +/// Every panel-backend command, grouped by module. +pub fn commands() -> Vec { + let mut table = Table(Vec::new()); + coprocessor(&mut table); + table.0 +} + +// --------------------------------------------------------------------------- +// Argument and response helpers +// --------------------------------------------------------------------------- + +/// Read argument `name` from `payload`, trying `name` and then its camelCase +/// form. A missing key reads as `null`, so `Option` yields `None`. +pub fn arg(payload: &Value, name: &str) -> Result { + let value = lookup(payload, name).cloned().unwrap_or(Value::Null); + let missing = value.is_null(); + serde_json::from_value(value).map_err(|e| { + if missing { + format!("missing argument `{name}`") + } else { + format!("invalid argument `{name}`: {e}") + } + }) +} + +/// Read a struct argument: the value under `name` (or its camelCase form) if +/// present, otherwise the whole payload. The old frontend sent both shapes. +pub fn struct_arg(payload: &Value, name: &str) -> Result { + let value = lookup(payload, name).unwrap_or(payload).clone(); + serde_json::from_value(value).map_err(|e| format!("invalid argument `{name}`: {e}")) +} + +/// Wrap a handler result in the IPC response envelope. +pub fn respond(result: Result) -> Value { + match result { + Ok(text) => { + let result = serde_json::from_str::(&text).unwrap_or(Value::String(text)); + json!({ "ok": true, "result": result }) + } + Err(error) => json!({ "ok": false, "error": error }), + } +} + +/// Run an `async fn` handler to completion on the shared IPC runtime. +/// +/// Must not be called from inside a Tokio runtime. Gossamer's worker threads +/// are plain threads, so `command_async` handlers can call it. +pub fn block_on(future: F) -> Result +where + F: Future>, +{ + static RUNTIME: OnceLock> = OnceLock::new(); + let runtime = RUNTIME.get_or_init(|| { + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .map_err(|e| format!("cannot start the IPC Tokio runtime: {e}")) + }); + match runtime { + Ok(runtime) => runtime.block_on(future), + Err(e) => Err(e.clone()), + } +} + +fn lookup<'a>(payload: &'a Value, name: &str) -> Option<&'a Value> { + payload + .get(name) + .or_else(|| payload.get(snake_to_camel(name).as_str())) +} + +fn snake_to_camel(name: &str) -> String { + let mut out = String::with_capacity(name.len()); + let mut upper = false; + for c in name.chars() { + if c == '_' { + upper = true; + } else if upper { + out.extend(c.to_uppercase()); + upper = false; + } else { + out.push(c); + } + } + out +} + +struct Table(Vec); + +impl Table { + fn add(&mut self, name: &'static str, required: &'static [&'static str], f: F) + where + F: Fn(&Value) -> Result + Send + 'static, + { + self.0.push(Command { + name, + required, + handler: Box::new(move |payload| Ok(respond(f(&payload)))), + }); + } +} + +// --------------------------------------------------------------------------- +// Command groups +// --------------------------------------------------------------------------- + +fn coprocessor(t: &mut Table) { + use crate::coprocessor::commands as c; + t.add("query_compute_engine", &["engine_id", "operation"], |p| { + block_on(c::query_compute_engine( + arg(p, "engine_id")?, + arg(p, "operation")?, + )) + }); + t.add("discover_compute_devices", &[], |_| { + block_on(c::discover_compute_devices()) + }); + t.add("coprocessor_load_ffi", &["lib_path"], |p| { + block_on(c::coprocessor_load_ffi(arg(p, "lib_path")?)) + }); + t.add( + "coprocessor_ffi_dispatch", + &["backend", "operation", "payload"], + |p| { + block_on(c::coprocessor_ffi_dispatch( + arg(p, "backend")?, + arg(p, "operation")?, + arg(p, "payload")?, + )) + }, + ); + t.add("coprocessor_ffi_status", &[], |_| { + block_on(c::coprocessor_ffi_status()) + }); + t.add("coprocessor_dispatch_local", &["operation", "input"], |p| { + block_on(c::coprocessor_dispatch_local( + arg(p, "operation")?, + arg(p, "input")?, + )) + }); + t.add("coprocessor_check_ffi", &[], |_| { + block_on(c::coprocessor_check_ffi()) + }); + t.add("coprocessor_benchmark", &[], |_| { + block_on(c::coprocessor_benchmark()) + }); + t.add("coprocessor_local_resources", &[], |_| { + block_on(c::coprocessor_local_resources()) + }); + t.add( + "coprocessor_smart_dispatch", + &["operation", "payload"], + |p| { + block_on(c::coprocessor_smart_dispatch( + arg(p, "operation")?, + arg(p, "payload")?, + )) + }, + ); +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashSet; + + fn call(name: &str, payload: Value) -> Value { + let commands = commands(); + let cmd = commands + .iter() + .find(|c| c.name == name) + .unwrap_or_else(|| panic!("no command {name}")); + (cmd.handler)(payload).expect("handlers always return Ok") + } + + #[test] + fn arg_reads_snake_then_camel() { + let p = + json!({ "panel_id": "a", "repoPath": "b", "both_set": "snake", "bothSet": "camel" }); + assert_eq!(arg::(&p, "panel_id").unwrap(), "a"); + assert_eq!(arg::(&p, "repo_path").unwrap(), "b"); + assert_eq!(arg::(&p, "both_set").unwrap(), "snake"); + } + + #[test] + fn arg_missing_required_and_optional() { + let p = json!({}); + assert_eq!( + arg::(&p, "lib_path").unwrap_err(), + "missing argument `lib_path`" + ); + assert_eq!(arg::>(&p, "port").unwrap(), None); + assert_eq!( + arg::>(&json!({ "port": 8080 }), "port").unwrap(), + Some(8080) + ); + let err = arg::(&json!({ "port": "x" }), "port").unwrap_err(); + assert!(err.starts_with("invalid argument `port`"), "{err}"); + } + + #[test] + fn struct_arg_accepts_nested_or_flattened() { + #[derive(serde::Deserialize, Debug, PartialEq)] + struct Req { + content: String, + } + let nested = json!({ "request": { "content": "hi" } }); + let flat = json!({ "content": "hi" }); + assert_eq!(struct_arg::(&nested, "request").unwrap().content, "hi"); + assert_eq!(struct_arg::(&flat, "request").unwrap().content, "hi"); + assert!(struct_arg::(&json!({}), "request") + .unwrap_err() + .contains("missing")); + } + + #[test] + fn respond_envelope() { + assert_eq!( + respond(Ok(r#"{"a":1}"#.into())), + json!({ "ok": true, "result": { "a": 1 } }) + ); + assert_eq!( + respond(Ok("plain text".into())), + json!({ "ok": true, "result": "plain text" }) + ); + assert_eq!( + respond(Err("boom".into())), + json!({ "ok": false, "error": "boom" }) + ); + } + + #[test] + fn snake_to_camel_cases() { + assert_eq!(snake_to_camel("provider_id"), "providerId"); + assert_eq!(snake_to_camel("base64_data"), "base64Data"); + assert_eq!(snake_to_camel("path"), "path"); + } + + #[test] + fn command_names_are_unique_and_disjoint_from_main_rs() { + let mut seen = HashSet::new(); + for cmd in commands() { + assert!(seen.insert(cmd.name), "duplicate command {}", cmd.name); + } + // Commands main.rs registers itself must not be registered twice. + let main_rs = include_str!("main.rs"); + for chunk in main_rs.split("app.command(\"").skip(1) { + let name = chunk.split('"').next().unwrap_or_default(); + assert!( + !seen.contains(name), + "{name} is registered in main.rs and the table" + ); + } + } + + /// Every command with a required argument must reject an empty payload + /// before its handler body runs (so this test has no side effects). + #[test] + fn required_arguments_are_enforced() { + for cmd in commands() { + if cmd.required.is_empty() { + continue; + } + let response = (cmd.handler)(json!({})).expect("handlers always return Ok"); + assert_eq!( + response["ok"], false, + "{} accepted an empty payload", + cmd.name + ); + let error = response["error"].as_str().unwrap_or_default(); + assert!( + error.contains("missing"), + "{}: unexpected error {error}", + cmd.name + ); + } + } + + #[test] + fn coprocessor_commands_run_through_the_table() { + let status = call("coprocessor_ffi_status", json!({})); + assert_eq!(status["ok"], true, "{status}"); + assert_eq!(status["result"]["ffi_loaded"], false); + + let load = call( + "coprocessor_load_ffi", + json!({ "libPath": "/no/such/libpanll_copro.so" }), + ); + assert_eq!(load["ok"], false, "{load}"); + + let missing = call("query_compute_engine", json!({ "operation": "x" })); + assert_eq!(missing["error"], "missing argument `engine_id`"); + } +} diff --git a/src-gossamer/src/lib.rs b/src-gossamer/src/lib.rs index 70964f45..97328a31 100644 --- a/src-gossamer/src/lib.rs +++ b/src-gossamer/src/lib.rs @@ -32,13 +32,14 @@ pub mod llm_coding; /// Coprocessor — external compute control plane + Zig FFI data plane. /// -/// Previously orphaned (declared by no crate root, so never compiled). Wired -/// into the library here so it builds, lints, and its tests run. Note: the -/// async command handlers are not yet registered in the binary's IPC table — -/// that integration is tracked as follow-up debt (see -/// `docs/TECHNICAL_DEBT.adoc`, F1/F2). +/// Previously orphaned (declared by no crate root, so never compiled). Its +/// command handlers are registered through [`ipc`]. pub mod coprocessor; +/// IPC command table — adapts the panel-backend handlers to Gossamer's IPC +/// handler shape. `main.rs` registers every entry with `command_async`. +pub mod ipc; + // --------------------------------------------------------------------------- // Formerly orphaned modules — panel backends, live service connections // (`*_live`), and shared helpers (`compat`, `error`). diff --git a/src-gossamer/src/main.rs b/src-gossamer/src/main.rs index f2ceaa63..f8351aba 100644 --- a/src-gossamer/src/main.rs +++ b/src-gossamer/src/main.rs @@ -404,4 +404,16 @@ fn register_commands(app: &mut gossamer_rs::App) { let url = format!("{}/state/{}", verisim_url(), key); result_str_to_json(blocking_get(&url, 10)) }); + + // ----------------------------------------------------------------------- + // Panel-backend commands (the lib's IPC table) + // + // `command_async` runs each call on a Gossamer worker thread, so blocking + // I/O and `async fn` handlers never stall the GTK main thread. Argument + // and response conventions are documented in `panll::ipc`. + // ----------------------------------------------------------------------- + + for cmd in panll::ipc::commands() { + app.command_async(cmd.name, cmd.handler); + } } From 2ba1ef89bc58d8504f35822edaa669507a5b40bb Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 12:00:07 +0000 Subject: [PATCH 2/5] ci: repin the standards reusables past the lockfile fix (standards#1271) GitHub's runner now enforces actions.lock inside reusable workflows, and for a reusable it reads the defining repository's lock. standards@092deda records actions/checkout@3d3c42e5 with `ref:` set to the SHA itself, which the runner rejects as "invalid lockfile pin". Hypatia (scheduled 18 Oct) and Scorecards (19 Oct) on main failed at job setup with exactly that; the 14 Oct push runs, on an older runner build, passed on the same commit. standards fixed its lock in f335165a (#1271). - Governance, Hypatia, Scorecards, Secret Scanner and Mirror move from standards@092deda to standards@75bd599 (main, 2026-10-19). Every input and secret the reusables gained since is optional. - secrets: inherit is narrowed to what each callee declares, as standards did for its own callers (#1283/#1284): HYPATIA_SCAN_PAT for Governance and Hypatia, the seven named forge keys for Mirror, nothing for Secret Scanner and Scorecards. - actions.lock: the standards@75bd599 record lists the external uses: of the five called files (the shape close-lock.sh writes, #1247), and each of those has a leaf record copied from standards' own lock at 75bd599 (tag refs, re-checked against git ls-remote). The record I added in #150 for actions/checkout@3d3c42e5 had the same SHA-as-ref defect; it is now ref v7.0.1. Rust CI stays on standards@412a703, which has no lockfile to enforce. Verified locally with standards' check-actions-lock-gate.sh and update-actions-lock.sh --verify-local at 75bd599, gh-actions-lock v0.2.0. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BCQeL169ZnUirUVaDXQLA3 (cherry picked from commit 689a79ef7ba4f91128ee27555987b41f48fedcfe) --- .github/workflows/actions.lock | 84 ++++++++++++++++++++++++---- .github/workflows/governance.yml | 4 +- .github/workflows/hypatia-scan.yml | 5 +- .github/workflows/mirror.yml | 11 +++- .github/workflows/scorecard.yml | 3 +- .github/workflows/secret-scanner.yml | 3 +- 6 files changed, 90 insertions(+), 20 deletions(-) diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock index c8117ccb..58daaa9c 100644 --- a/.github/workflows/actions.lock +++ b/.github/workflows/actions.lock @@ -28,9 +28,9 @@ workflows: - 'goto-bus-stop/setup-zig@v2.2.1' - 'swatinem/rust-cache@v2.9.2' '.github/workflows/governance.yml': - - 'hyperpolymath/standards@092dedada188f56c5915f74a5fd40aac093742c3' + - 'hyperpolymath/standards@75bd5994470661a099df562d6d5ad2fe744dbd66' '.github/workflows/hypatia-scan.yml': - - 'hyperpolymath/standards@092dedada188f56c5915f74a5fd40aac093742c3' + - 'hyperpolymath/standards@75bd5994470661a099df562d6d5ad2fe744dbd66' '.github/workflows/instant-sync.yml': - 'peter-evans/repository-dispatch@v4.0.1' '.github/workflows/label-triage.yml': [] @@ -38,23 +38,28 @@ workflows: '.github/workflows/main-estate-audit.yml': - 'hyperpolymath/cicd-suite@4f5404281dd7092fa77cc18daec7237c4f1ee4ac' '.github/workflows/mirror.yml': - - 'hyperpolymath/standards@092dedada188f56c5915f74a5fd40aac093742c3' + - 'hyperpolymath/standards@75bd5994470661a099df562d6d5ad2fe744dbd66' '.github/workflows/push-email-notify.yml': - 'hyperpolymath/smtp-notify-action@v0.5.0' '.github/workflows/rust-ci.yml': - 'hyperpolymath/standards@412a7031577112b31ee287cc6060179d638d6500' '.github/workflows/scorecard.yml': - - 'hyperpolymath/standards@092dedada188f56c5915f74a5fd40aac093742c3' + - 'hyperpolymath/standards@75bd5994470661a099df562d6d5ad2fe744dbd66' '.github/workflows/secret-scanner.yml': - - 'hyperpolymath/standards@092dedada188f56c5915f74a5fd40aac093742c3' + - 'hyperpolymath/standards@75bd5994470661a099df562d6d5ad2fe744dbd66' dependencies: + 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9': + ref: 'v6.1.0' + commit: 'sha1-55cc8345863c7cc4c66a329aec7e433d2d1c52a9' + owner_id: 44036562 + repo_id: 215566462 'actions/cache@v6.1.0': ref: 'v6.1.0' commit: 'sha1-55cc8345863c7cc4c66a329aec7e433d2d1c52a9' owner_id: 44036562 repo_id: 215566462 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1': - ref: '3d3c42e5aac5ba805825da76410c181273ba90b1' + ref: 'v7.0.1' commit: 'sha1-3d3c42e5aac5ba805825da76410c181273ba90b1' owner_id: 44036562 repo_id: 197814629 @@ -78,6 +83,11 @@ dependencies: commit: 'sha1-bbbca2ddaa5d8feaa63e36b76fdaad77386f024f' owner_id: 44036562 repo_id: 192625955 + 'actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9': + ref: 'v7.0.2' + commit: 'sha1-cf430e030ddbb5b0abf93d22962f4752f3646cd9' + owner_id: 44036562 + repo_id: 192625955 'actions/upload-pages-artifact@v5.0.0': ref: 'v5.0.0' commit: 'sha1-fc324d3547104276b827a68afc52ff2a11cc49c9' @@ -85,11 +95,36 @@ dependencies: repo_id: 496012378 uses: - 'actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f' + 'denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed': + ref: 'v2.0.5' + commit: 'sha1-22d081ff2d3a40755e97629de92e3bcbfa7cf2ed' + owner_id: 42048915 + repo_id: 356423100 + 'dtolnay/rust-toolchain@e2a55d2ffb04f378e9626c28d38b36d230d1e12f': + ref: 'v1' + commit: 'sha1-e2a55d2ffb04f378e9626c28d38b36d230d1e12f' + owner_id: 1940490 + repo_id: 260749683 'dtolnay/rust-toolchain@v1': ref: 'v1' commit: 'sha1-e2a55d2ffb04f378e9626c28d38b36d230d1e12f' owner_id: 1940490 repo_id: 260749683 + 'editorconfig-checker/action-editorconfig-checker@51f63319f592f97930c73d9c46184d20bd206393': + ref: 'v3.0.0' + commit: 'sha1-51f63319f592f97930c73d9c46184d20bd206393' + owner_id: 26415196 + repo_id: 297874902 + 'erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124': + ref: 'v1.24.1' + commit: 'sha1-54075bcc5e249e4758d363f27d099f55d843f124' + owner_id: 47606891 + repo_id: 331103973 + 'github/codeql-action@b96794f015dfd88f77b49b1c93e0fa7110f94c63': + ref: 'v4.38.0' + commit: 'sha1-b96794f015dfd88f77b49b1c93e0fa7110f94c63' + owner_id: 9919 + repo_id: 259445878 'github/codeql-action@v4.38.3': ref: 'v4.38.3' commit: 'sha1-24c54180a607b1449ed407dd24f251e4e9147c8d' @@ -146,16 +181,38 @@ dependencies: commit: 'sha1-c1c9fa07992a02c1fd3d67a0dc1b08cccb852aef' owner_id: 6759885 repo_id: 1352485172 - 'hyperpolymath/standards@092dedada188f56c5915f74a5fd40aac093742c3': - ref: '092dedada188f56c5915f74a5fd40aac093742c3' - commit: 'sha1-092dedada188f56c5915f74a5fd40aac093742c3' - owner_id: 6759885 - repo_id: 1116521501 'hyperpolymath/standards@412a7031577112b31ee287cc6060179d638d6500': ref: '412a7031577112b31ee287cc6060179d638d6500' commit: 'sha1-412a7031577112b31ee287cc6060179d638d6500' owner_id: 6759885 repo_id: 1116521501 + 'hyperpolymath/standards@75bd5994470661a099df562d6d5ad2fe744dbd66': + ref: '75bd5994470661a099df562d6d5ad2fe744dbd66' + commit: 'sha1-75bd5994470661a099df562d6d5ad2fe744dbd66' + owner_id: 6759885 + repo_id: 1116521501 + uses: + - 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9' + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' + - 'actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9' + - 'denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed' + - 'dtolnay/rust-toolchain@e2a55d2ffb04f378e9626c28d38b36d230d1e12f' + - 'editorconfig-checker/action-editorconfig-checker@51f63319f592f97930c73d9c46184d20bd206393' + - 'erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124' + - 'github/codeql-action@b96794f015dfd88f77b49b1c93e0fa7110f94c63' + - 'ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc' + - 'oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6' + - 'webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555' + 'ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc': + ref: 'v2.4.4' + commit: 'sha1-2d1146689b8cda280b9bc96326124645441f03bc' + owner_id: 67707773 + repo_id: 421101922 + 'oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6': + ref: 'v2.2.0' + commit: 'sha1-0c5077e51419868618aeaa5fe8019c62421857d6' + owner_id: 108928776 + repo_id: 512644635 'peter-evans/repository-dispatch@v4.0.1': ref: 'v4.0.1' commit: 'sha1-28959ce8df70de7be546dd1250a005dd32156697' @@ -166,3 +223,8 @@ dependencies: commit: 'sha1-6323deb102c322ba6fcbdcafc7e3dddab59af2b6' owner_id: 580492 repo_id: 298565987 + 'webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555': + ref: 'v0.10.0' + commit: 'sha1-e83874834305fe9a4a2997156cb26c5de65a8555' + owner_id: 135788 + repo_id: 208510314 diff --git a/.github/workflows/governance.yml b/.github/workflows/governance.yml index c707088a..95eafe72 100644 --- a/.github/workflows/governance.yml +++ b/.github/workflows/governance.yml @@ -33,4 +33,6 @@ permissions: jobs: governance: - uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@092dedada188f56c5915f74a5fd40aac093742c3 + uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@75bd5994470661a099df562d6d5ad2fe744dbd66 + secrets: + HYPATIA_SCAN_PAT: ${{ secrets.HYPATIA_SCAN_PAT }} diff --git a/.github/workflows/hypatia-scan.yml b/.github/workflows/hypatia-scan.yml index 25fe50d4..06edf194 100644 --- a/.github/workflows/hypatia-scan.yml +++ b/.github/workflows/hypatia-scan.yml @@ -27,5 +27,6 @@ permissions: jobs: hypatia: - uses: hyperpolymath/standards/.github/workflows/hypatia-scan-reusable.yml@092dedada188f56c5915f74a5fd40aac093742c3 - secrets: inherit + uses: hyperpolymath/standards/.github/workflows/hypatia-scan-reusable.yml@75bd5994470661a099df562d6d5ad2fe744dbd66 + secrets: + HYPATIA_SCAN_PAT: ${{ secrets.HYPATIA_SCAN_PAT }} diff --git a/.github/workflows/mirror.yml b/.github/workflows/mirror.yml index af1d68f8..6182ddfd 100644 --- a/.github/workflows/mirror.yml +++ b/.github/workflows/mirror.yml @@ -13,5 +13,12 @@ permissions: jobs: mirror: - uses: hyperpolymath/standards/.github/workflows/mirror-reusable.yml@092dedada188f56c5915f74a5fd40aac093742c3 - secrets: inherit + uses: hyperpolymath/standards/.github/workflows/mirror-reusable.yml@75bd5994470661a099df562d6d5ad2fe744dbd66 + secrets: + GITLAB_SSH_KEY: ${{ secrets.GITLAB_SSH_KEY }} + BITBUCKET_SSH_KEY: ${{ secrets.BITBUCKET_SSH_KEY }} + CODEBERG_SSH_KEY: ${{ secrets.CODEBERG_SSH_KEY }} + SOURCEHUT_SSH_KEY: ${{ secrets.SOURCEHUT_SSH_KEY }} + DISROOT_SSH_KEY: ${{ secrets.DISROOT_SSH_KEY }} + GITEA_SSH_KEY: ${{ secrets.GITEA_SSH_KEY }} + RADICLE_KEY: ${{ secrets.RADICLE_KEY }} diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 57473e9e..47ce57a7 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -16,5 +16,4 @@ jobs: contents: read security-events: write id-token: write - uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@092dedada188f56c5915f74a5fd40aac093742c3 - secrets: inherit + uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@75bd5994470661a099df562d6d5ad2fe744dbd66 diff --git a/.github/workflows/secret-scanner.yml b/.github/workflows/secret-scanner.yml index ebe54aef..f5e9834a 100644 --- a/.github/workflows/secret-scanner.yml +++ b/.github/workflows/secret-scanner.yml @@ -21,5 +21,4 @@ jobs: contents: read pull-requests: write actions: read - uses: hyperpolymath/standards/.github/workflows/secret-scanner-reusable.yml@092dedada188f56c5915f74a5fd40aac093742c3 - secrets: inherit + uses: hyperpolymath/standards/.github/workflows/secret-scanner-reusable.yml@75bd5994470661a099df562d6d5ad2fe744dbd66 From e2250b024a0abffcd58cf3bf8ece67872f7a9247 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 12:04:54 +0000 Subject: [PATCH 3/5] ci(secrets): ignore four historical gitleaks fingerprints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit standards@75bd599's secret scanner adds a full-history gitleaks pass. On this PR it found four historical findings, none a credential, none in the current tree: - docs/api-reference.md at aff0781: the placeholder snapshot id in the API examples (replaced in #140); - tests/aspect/security_test.mjs at 9eed9ac: synthetic ABCDEFGHIJ… OpenAI/GitHub-shaped fixtures for the redaction tests (built at runtime by syntheticToken() since #122). .gitleaksignore lists exactly those commit:file:rule:line fingerprints, the format standards uses for its own history. Reproduced with the pinned gitleaks 8.18.4 and the estate baseline: 4 leaks before, none after. Canary: the same placeholder committed again is still caught. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BCQeL169ZnUirUVaDXQLA3 (cherry picked from commit 926742f8f7df7ce1a3d2ac1289546cafca72d4e3) --- .gitleaksignore | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .gitleaksignore diff --git a/.gitleaksignore b/.gitleaksignore new file mode 100644 index 00000000..05ef7fe1 --- /dev/null +++ b/.gitleaksignore @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: MPL-2.0 +# +# Exact historical fingerprints only (commit:file:rule:line), for the +# full-history gitleaks pass in standards' secret-scanner-reusable. None is a +# real credential, and the current tree no longer carries any of them: +# - docs/api-reference.md: the placeholder snapshot id `abc123-def456` in the +# API examples, replaced by an obviously fake id in #140. +# - tests/aspect/security_test.mjs: synthetic OpenAI/GitHub-shaped tokens +# (ABCDEFGHIJ… fixtures) for the redaction tests, now built at runtime by +# syntheticToken() since #122. +# These entries suppress nothing in any other commit or location. +aff07811392d0b2a884f08284a0f4e8ab0e1cc68:docs/api-reference.md:generic-api-key:38 +aff07811392d0b2a884f08284a0f4e8ab0e1cc68:docs/api-reference.md:generic-api-key:72 +9eed9ac0fe21b99aed24be00d9c43007d8df3046:tests/aspect/security_test.mjs:generic-api-key:263 +9eed9ac0fe21b99aed24be00d9c43007d8df3046:tests/aspect/security_test.mjs:generic-api-key:269 From 3a8a369cd2c71ff873f70df650e15f867f0ade14 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 12:19:01 +0000 Subject: [PATCH 4/5] ci(e2e): key the Rust cache on the CPU model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gossamer-rs's build.rs compiles libgossamer with `zig build-lib … -OReleaseSafe` and no -target or -mcpu, so Zig targets the build host's CPU. The archive also bundles Zig's compiler_rt, which exports memcpy/memset, so every binary that links it runs that code, even the bin's empty test harness. e2e.yml cached target/, including that archive, under a key that ignores the CPU. ubuntu-latest mixes CPU models, so a cache saved on one runner and restored on another can die with SIGILL. That happened on hyperpolymath/panll#155: - run 114 missed the cache, built everything, passed and saved the cache; - run 116, on byte-identical Rust source, restored it on a full key match, rebuilt only `panll`, and the panll-gossamer test harness hit SIGILL at startup. The cache key now includes a hash of /proc/cpuinfo's model name, so target/ is reused only on the same CPU model. The durable fix is upstream: gossamer-rs's build.rs should pass a portable -mcpu. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SJtjqoZaSQo7uBwvwVMA9G (cherry picked from commit f9e7fa7077493f8d64920a3821b8948ca2fa8c24) --- .github/workflows/e2e.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 4c9283cd..f4c74c49 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -54,8 +54,17 @@ jobs: with: toolchain: stable + # gossamer-rs builds libgossamer with Zig for the host CPU (no -mcpu), so + # a target/ cache restored on a runner with another CPU model can die + # with SIGILL. Key the cache on the CPU model. + - name: CPU model for the cache key + id: cpu + run: echo "model=$(grep -m1 'model name' /proc/cpuinfo | sha256sum | cut -c1-12)" >> "$GITHUB_OUTPUT" + - name: Rust cache uses: Swatinem/rust-cache@v2.9.2 + with: + key: cpu-${{ steps.cpu.outputs.model }} - name: Install GTK/WebKit dev headers run: sudo apt-get update && sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev From 6310eaf668af4a51f329621dc42454b4eee2ee9d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 12:21:58 +0000 Subject: [PATCH 5/5] fix(ipc): coprocessor_load_ffi's lib_path is optional coprocessor_load_ffi treats an empty lib_path as "use runtime discovery" (ffi_so_path()), and the old frontend's "Load Local FFI" action sent no path at all ({"_": true}). The table had made lib_path required, so that call failed with "missing argument" and discovery was unreachable. lib_path is now read as Option and defaults to empty. Found by Codex review on hyperpolymath/panll#157. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SJtjqoZaSQo7uBwvwVMA9G --- src-gossamer/src/ipc.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src-gossamer/src/ipc.rs b/src-gossamer/src/ipc.rs index a6c85b39..bfb0ca0d 100644 --- a/src-gossamer/src/ipc.rs +++ b/src-gossamer/src/ipc.rs @@ -157,8 +157,11 @@ fn coprocessor(t: &mut Table) { t.add("discover_compute_devices", &[], |_| { block_on(c::discover_compute_devices()) }); - t.add("coprocessor_load_ffi", &["lib_path"], |p| { - block_on(c::coprocessor_load_ffi(arg(p, "lib_path")?)) + // An empty `lib_path` means "use runtime discovery", which is how the + // "Load Local FFI" action calls it (no path at all). + t.add("coprocessor_load_ffi", &[], |p| { + let lib_path: Option = arg(p, "lib_path")?; + block_on(c::coprocessor_load_ffi(lib_path.unwrap_or_default())) }); t.add( "coprocessor_ffi_dispatch", @@ -330,6 +333,15 @@ mod tests { ); assert_eq!(load["ok"], false, "{load}"); + // `lib_path` is optional: omitting it selects runtime discovery + // rather than failing as a missing argument. + let commands = commands(); + let load_ffi = commands + .iter() + .find(|c| c.name == "coprocessor_load_ffi") + .expect("coprocessor_load_ffi is registered"); + assert!(load_ffi.required.is_empty()); + let missing = call("query_compute_engine", json!({ "operation": "x" })); assert_eq!(missing["error"], "missing argument `engine_id`"); }