Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
27184b8
Trigger main CI checks
think-in-universe Jul 3, 2026
0d5062c
Enable main-equivalent checks on CI probe PR
think-in-universe Jul 3, 2026
0fec671
Run push-main checks on CI probe branch
think-in-universe Jul 3, 2026
6cecff6
Narrow CI probe to failed main checks
think-in-universe Jul 3, 2026
2facb02
fix ci failures for clippy and coverage
italic-jinxin Jul 3, 2026
614554f
fix dockerfile runtime home clippy on windows
italic-jinxin Jul 3, 2026
ef787eb
fix credential header test coverage flake
italic-jinxin Jul 3, 2026
d7ea4a6
fix reborn composition coverage flakes
italic-jinxin Jul 3, 2026
d40c183
fix budget gate snapshot serde under feature union
italic-jinxin Jul 3, 2026
cb940f1
fix postgres filesystem create dir races
italic-jinxin Jul 3, 2026
22ec0c6
revert ci probe branch triggers
italic-jinxin Jul 3, 2026
47c7d30
Merge branch 'main' into codex/main-ci-checks
italic-jinxin Jul 3, 2026
82a534c
fix wasm wasi clippy feature build
italic-jinxin Jul 3, 2026
3e98ecf
address ci review hardening
italic-jinxin Jul 4, 2026
e5aada6
Merge branch 'main' into codex/main-ci-checks
italic-jinxin Jul 4, 2026
9e67419
fix resource limits max usd legacy decode
italic-jinxin Jul 4, 2026
29facf4
trigger main ci on probe branch
italic-jinxin Jul 4, 2026
6de925c
address review hardening follow-ups
italic-jinxin Jul 4, 2026
b4ebd09
align responses api path tests with reborn router
italic-jinxin Jul 4, 2026
1a9d9e4
fix responses api temperature on reborn router
italic-jinxin Jul 4, 2026
fcf7e13
document responses temperature bounds
italic-jinxin Jul 4, 2026
418eece
align retired engine thread isolation tests
italic-jinxin Jul 4, 2026
f48a08a
restore main-only ci push triggers
italic-jinxin Jul 4, 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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 7 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ exclude = [
rust-version = "1.96"

[workspace.dependencies]
# WASM sandbox for untrusted tool execution.
# Floor 46.0.1: minimum current major patch containing the fixes for the
# Wasmtime WASI FilePerms advisories RUSTSEC-2026-0149 and RUSTSEC-2026-0188.
wasmtime = { version = "46.0.1", features = ["component-model"] }
# Only the WASI p2 host API is linked (`p2::add_to_linker_sync`). Keeping
# defaults off avoids compiling unused p0/p1 WITX proc macros under remote CI
# sccache workers, where registry sidecar WITX files are not present.
Expand Down Expand Up @@ -187,11 +191,8 @@ open = "5"
# The postgres feature provides ToSql/FromSql for postgres-types (shared by tokio-postgres)
pgvector = { version = "0.4", features = ["postgres"], optional = true }

# WASM sandbox for untrusted tool execution.
# Floor 46.0.1: minimum current major patch containing the fixes for the
# Wasmtime WASI FilePerms advisories RUSTSEC-2026-0149 and RUSTSEC-2026-0188.
wasmtime = { version = "46.0.1", features = ["component-model"] }
wasmtime-wasi.workspace = true
wasmtime = { workspace = true }
wasmtime-wasi = { workspace = true } # WASI support for component model
wasmparser = "0.250.0" # WASM binary parsing for validation

# Cryptography for secrets management
Expand Down Expand Up @@ -293,6 +294,7 @@ ironclaw_product_workflow = { path = "crates/ironclaw_product_workflow", version
ironclaw_reborn = { path = "crates/ironclaw_reborn", version = "0.1.0", features = ["root-llm-provider"] }
ironclaw_reborn_composition = { path = "crates/ironclaw_reborn_composition", version = "0.1.0", features = ["test-support", "libsql"] }
ironclaw_reborn_config = { path = "crates/ironclaw_reborn_config", version = "0.1.0" }
ironclaw_reborn_openai_compat = { path = "crates/ironclaw_reborn_openai_compat", version = "0.1.0", features = ["openai-compat-beta"] }
ironclaw_run_state = { path = "crates/ironclaw_run_state", version = "0.1.0" }
ironclaw_threads = { path = "crates/ironclaw_threads", version = "0.1.0" }
# `test-support` unlocks `TrustedTriggerSubmitRequest::new_for_test` (E-TRIGGERED-SUBMIT
Expand Down
63 changes: 39 additions & 24 deletions crates/ironclaw_filesystem/src/postgres.rs
Original file line number Diff line number Diff line change
Expand Up @@ -774,37 +774,52 @@ impl RootFilesystem for PostgresRootFilesystem {
.transaction()
.await
.map_err(|error| db_error(path.clone(), FilesystemOperation::CreateDirAll, error))?;
for prefix in virtual_path_prefixes(path)? {
let row = transaction
.query_opt(
"SELECT is_dir FROM root_filesystem_entries WHERE path = $1",
&[&prefix.as_str()],
)
.await
.map_err(|error| {
db_error(prefix.clone(), FilesystemOperation::CreateDirAll, error)
let prefixes = virtual_path_prefixes(path)?;
let prefix_paths: Vec<&str> = prefixes.iter().map(VirtualPath::as_str).collect();

// Keep conflict detection inside the insert statement. `ON CONFLICT`
// waits for concurrent writers on the unique path key; if a file wins,
// the conditional no-op update returns that row as `is_dir = false`
// and the surrounding transaction rolls back.
let rows = transaction
.query(
r#"
INSERT INTO root_filesystem_entries (path, contents, is_dir)
SELECT prefix.path, ''::bytea, TRUE
FROM UNNEST($1::text[]) AS prefix(path)
ON CONFLICT (path) DO UPDATE
SET is_dir = root_filesystem_entries.is_dir
WHERE root_filesystem_entries.is_dir = FALSE
RETURNING path, is_dir
"#,
&[&prefix_paths],
)
.await
.map_err(|error| db_error(path.clone(), FilesystemOperation::CreateDirAll, error))?;

for row in rows {
let is_dir = row.try_get::<_, bool>("is_dir").map_err(|error| {
db_error(path.clone(), FilesystemOperation::CreateDirAll, error)
})?;
if !is_dir {
let raw_path = row.try_get::<_, String>("path").map_err(|error| {
db_error(path.clone(), FilesystemOperation::CreateDirAll, error)
})?;
let conflict_path = VirtualPath::new(raw_path.clone()).map_err(|error| {
FilesystemError::Backend {
path: path.clone(),
operation: FilesystemOperation::CreateDirAll,
reason: format!("invalid backend conflict path {raw_path:?}: {error}"),
}
})?;
if row.is_some_and(|row| !row.get::<_, bool>("is_dir")) {
return Err(FilesystemError::Backend {
path: prefix,
path: conflict_path,
operation: FilesystemOperation::CreateDirAll,
reason: "file exists where directory is required".to_string(),
});
}
transaction
.execute(
r#"
INSERT INTO root_filesystem_entries (path, contents, is_dir)
VALUES ($1, ''::bytea, TRUE)
ON CONFLICT (path) DO NOTHING
"#,
&[&prefix.as_str()],
)
.await
.map_err(|error| {
db_error(path.clone(), FilesystemOperation::CreateDirAll, error)
})?;
}

transaction
.commit()
.await
Expand Down
54 changes: 54 additions & 0 deletions crates/ironclaw_filesystem/tests/db_root_filesystem_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1637,6 +1637,60 @@ mod postgres_tests {
assert_eq!(fs.stat(&dir).await.unwrap().file_type, FileType::Directory);
}

#[tokio::test]
async fn postgres_create_dir_all_conflict_rolls_back_inserted_prefixes() {
let Some((fs, prefix)) = postgres_root().await else {
return;
};
let parent = vpath(&prefix, "mkdir_conflict");
let blocking_file = vpath(&prefix, "mkdir_conflict/file");
let child_under_file = vpath(&prefix, "mkdir_conflict/file/child");

fs.put(
&blocking_file,
Entry::bytes(b"already a file".to_vec()),
CasExpectation::Absent,
)
.await
.unwrap();

let err = fs
.create_dir_all(&child_under_file)
.await
.expect_err("existing file prefix must reject create_dir_all");
match err {
FilesystemError::Backend {
path,
operation,
reason,
} => {
assert_eq!(path, blocking_file);
assert_eq!(operation, FilesystemOperation::CreateDirAll);
assert!(
reason.contains("file exists where directory is required"),
"unexpected reason: {reason}"
);
}
other => panic!("expected create_dir_all Backend error, got: {other:?}"),
}
assert_eq!(
fs.get(&blocking_file).await.unwrap().unwrap().entry.body,
b"already a file"
);

fs.delete(&blocking_file).await.unwrap();
assert!(
matches!(
fs.stat(&parent).await,
Err(FilesystemError::NotFound {
operation: FilesystemOperation::Stat,
..
})
),
"failed create_dir_all must roll back explicit directory rows inserted before the conflict"
);
}

#[tokio::test]
async fn postgres_transaction_rollback_discards_prior_put_after_later_cas_conflict() {
let Some((fs, prefix)) = postgres_root().await else {
Expand Down
2 changes: 1 addition & 1 deletion crates/ironclaw_hooks/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ thiserror = "2"
tokio = { version = "1", features = ["time", "rt", "sync"] }
tracing = "0.1"
uuid = { version = "1", features = ["v4"] }
wasmtime = { version = "46.0.1", features = ["component-model"] }
wasmtime = { workspace = true }

[dev-dependencies]
tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread"] }
Expand Down
3 changes: 3 additions & 0 deletions crates/ironclaw_reborn_composition/src/projection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,9 @@ pub(crate) fn build_reborn_projection_services(
let projection: Arc<dyn EventProjectionService> =
Arc::new(ReplayEventProjectionService::from_runtime_log(event_log));
let live_updates = Arc::new(InMemoryProjectionUpdateSource::new(128));
// One counter per projection-services bundle keeps all live publishers in
// the same SSE cursor space; per-publisher counters can collide after a
// durable cursor has advanced.
let live_sequence = Arc::new(AtomicU64::new(0));
let event_stream_manager = Arc::new(EventStreamManager::from_services(
projection,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ pub(super) struct LiveSkillActivationObserver {
pub(crate) struct LiveProjectionPublisher {
update_source: Arc<InMemoryProjectionUpdateSource>,
actor_user_id: UserId,
// Shared by publishers from the same projection services so live cursors
// stay monotonic across progress, skill, and other projection updates.
next_sequence: Arc<AtomicU64>,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

Expand Down
10 changes: 10 additions & 0 deletions crates/ironclaw_reborn_composition/src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5098,6 +5098,9 @@ output_schema_ref = "schemas/write.output.json"

#[cfg(feature = "root-llm-provider")]
struct RuntimeEnvGuard {
// Serializes tokio tests that mutate the runtime env overlay. The
// set/remove helpers lock only the separate override map, not
// ENV_MUTEX, so restoration can safely run while this guard is held.
_async_lock: tokio::sync::MutexGuard<'static, ()>,
_env_lock: std::sync::MutexGuard<'static, ()>,
previous: Vec<(&'static str, Option<String>)>,
Expand Down Expand Up @@ -5138,6 +5141,13 @@ output_schema_ref = "schemas/write.output.json"
Some(value) => ironclaw_common::env_helpers::set_runtime_env(name, value),
None => ironclaw_common::env_helpers::remove_runtime_env(name),
}
if !std::thread::panicking() {
debug_assert_eq!(
ironclaw_common::env_helpers::env_or_override(name),
previous.clone(),
"RuntimeEnvGuard failed to restore {name}"
);
}
}
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/ironclaw_reborn_openai_compat/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,7 @@ fn is_allowed_param_root(segment: &str) -> bool {
| "previous_response_id"
| "response_id"
| "stream"
| "temperature"
| "tool_choice"
| "tools"
) && index.is_none_or(is_ascii_digits)
Expand Down
4 changes: 4 additions & 0 deletions crates/ironclaw_reborn_openai_compat/src/responses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ pub struct OpenAiResponsesCreateRequest {
pub input: OpenAiResponsesInput,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stream: Option<bool>,
/// Optional sampling temperature override. Must be in the inclusive
/// OpenAI-compatible range `[0.0, 2.0]`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub temperature: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub instructions: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
Expand Down
16 changes: 16 additions & 0 deletions crates/ironclaw_reborn_openai_compat/src/responses_workflow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1363,9 +1363,22 @@ pub(crate) fn parse_response_create_request(
OpenAiCompatHttpError::invalid_request(Some("body".to_string()))
})?;
crate::model_validation::validate_model_name(&request.model)?;
validate_temperature(request.temperature)?;
Ok(request)
}

fn validate_temperature(temperature: Option<f64>) -> Result<(), OpenAiCompatHttpError> {
let Some(temperature) = temperature else {
return Ok(());
};
if (0.0..=2.0).contains(&temperature) {
return Ok(());
}
Err(OpenAiCompatHttpError::invalid_request(Some(
"temperature".to_string(),
)))
}

fn responses_user_message_payload(
request: &OpenAiResponsesCreateRequest,
) -> Result<UserMessagePayload, OpenAiCompatHttpError> {
Expand Down Expand Up @@ -1413,6 +1426,9 @@ fn responses_input_to_product_text(
if let Some(context) = &request.x_context {
payload["context"] = serde_json::Value::String(responses_context_to_product_text(context));
}
if let Some(temperature) = request.temperature {
payload["temperature"] = serde_json::json!(temperature);
}
serde_json::to_string(&payload).map_err(|_| OpenAiCompatHttpError::internal())
}

Expand Down
2 changes: 2 additions & 0 deletions crates/ironclaw_reborn_openai_compat/tests/dto_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,12 @@ fn responses_create_request_accepts_text_or_item_input() {
"model": "gpt-reborn",
"input": "hello",
"stream": false,
"temperature": 0.42,
"previous_response_id": "resp_previous"
}))
.expect("text input");
assert!(matches!(text.input, OpenAiResponsesInput::Text(_)));
assert_eq!(text.temperature, Some(0.42));
assert_eq!(
text.previous_response_id
.as_ref()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ fn suspicious_error_params_are_dropped_instead_of_normalized() {
"input[12].content",
"response_id",
"idempotency_key",
"temperature",
] {
let error = OpenAiCompatHttpError::from_kind(
400,
Expand Down
38 changes: 38 additions & 0 deletions crates/ironclaw_resources/src/filesystem_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,44 @@ mod tests {
assert!(store2.list_pending(&scope).unwrap().is_empty());
}

#[test]
fn approved_gate_with_increased_decimal_limit_reloads() {
let backend = Arc::new(InMemoryBackend::new());
let scope = gate_scope("tenant-fs", "alice");
let scoped = scoped_resources_fs(Arc::clone(&backend), "tenant-fs", "alice");
let store = FilesystemBudgetGateStore::new(scoped);
let gate = sample_gate();
let id = gate.id;
let increased_limit = ResourceLimits {
max_usd: Some(dec!(1000.00)),
..ResourceLimits::default()
};

store.open(&scope, gate).unwrap();
store
.resolve(
&scope,
id,
BudgetGateOutcome::Approve {
increased_limit: increased_limit.clone(),
by: UserId::new("alice").unwrap(),
},
Utc::now(),
)
.unwrap();

let scoped2 = scoped_resources_fs(Arc::clone(&backend), "tenant-fs", "alice");
let store2 = FilesystemBudgetGateStore::new(scoped2);
let reloaded = store2.get(&scope, id).unwrap().unwrap();
assert!(matches!(
reloaded.status,
BudgetGateStatus::Approved {
increased_limit: ref reloaded_limit,
..
} if reloaded_limit == &increased_limit
));
}

#[test]
fn expire_pending_older_than_persists_terminal_state() {
let backend = Arc::new(InMemoryBackend::new());
Expand Down
Loading
Loading