Skip to content

Commit 6603185

Browse files
committed
Merge remote-tracking branch 'origin/main' into codex/code-search-hardening-20260722
# Conflicts: # Cargo.lock
2 parents e42684f + 2636193 commit 6603185

29 files changed

Lines changed: 463 additions & 129 deletions

File tree

CHANGELOG.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [6.3.1] - 2026-07-22
11+
12+
### Fixed
13+
14+
- Removed redundant nested SRT write-deny mounts when a protected workspace
15+
ancestor already blocks writes, while retaining credential read denial and
16+
standalone sensitive-file write protection.
17+
18+
## [6.3.0] - 2026-07-22
19+
20+
### Added
21+
22+
- Added typed model-generation concurrency and admission contracts so hosts can
23+
declare and enforce bounded active generations without provider-name checks.
24+
- Added durable `MemoryObservation` and `MemoryObserver` extension points for
25+
auditable preference, skill, and knowledge projections after persistence.
26+
- Added run-scoped permission and confirmation snapshots plus targeted
27+
cancellation and expiry for pending tool approvals.
28+
29+
### Changed
30+
31+
- Serialized completed-turn memory extraction, drained accepted extractions at
32+
session close, and preserved canonical duplicate-consolidation results for
33+
observers.
34+
1035
### Fixed
1136

1237
- Kept LLM-authored learning titles, summaries, and instructions concise and
1338
user-facing, and excluded internal orchestration or handoff procedures from
1439
the learning signal contract.
40+
41+
## [6.2.0] - 2026-07-22
42+
43+
### Added
44+
45+
- Added a fail-closed SRT-backed `BashSandbox` with bounded execution,
46+
separated output streams, protected workspace control metadata, credential
47+
read boundaries, and verified exact-path npm and Node runtime constructors.
48+
49+
### Fixed
50+
1551
- Forwarded delegated child confirmation-required, confirmation-received, and
1652
confirmation-timeout events through the parent runtime stream so shared HITL
1753
providers cannot deadlock while the host UI waits for an event it never saw.

Cargo.lock

Lines changed: 5 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -321,9 +321,12 @@ language/bootstrap injection variables. It keeps stdout and stderr distinct
321321
under one global capture limit. Its deadline covers output draining and the
322322
child's complete lifetime, including a process that closes both streams before
323323
it exits; timeout or cancellation terminates the Unix process group. It never
324-
falls back to the host runner when `srt` is missing or fails. The embedding
325-
host remains responsible for choosing whether an unavailable sandbox causes
326-
an interactive escalation or a deterministic denial.
324+
searches `PATH` for a sandbox runtime and never falls back to the host runner
325+
when its explicitly provisioned runtime is missing or fails. Write-deny paths
326+
already covered by a protected ancestor are collapsed before SRT startup,
327+
while more-specific credential read denies remain intact. The embedding host
328+
remains responsible for choosing whether an unavailable sandbox causes an
329+
interactive escalation or a deterministic denial.
327330

328331
Shell isolation does not automatically govern in-process workspace tools.
329332
Interactive hosts should construct `LocalWorkspaceBackend` or
@@ -338,19 +341,19 @@ NUL-delimited format and regenerates output only for allowed paths; option-like
338341
revision input cannot become a Git flag, and displayed remote URLs omit
339342
embedded HTTP credentials and query tokens.
340343

341-
Hosts may use `discover` for a compatible npm installation on `PATH`, or
342-
`from_verified_npm_with_node` when a lifecycle owner supplies exact SRT and Node
343-
paths. Both verified constructors require the expected npm package identity and
344-
a tested SRT version. The A3S CLI uses the exact-path form after validating and,
345-
when policy allows, preparing its user-wide managed installation; Core does not
346-
install host software.
347-
348-
This adapter is a low-latency local enforcement provider, not the A3S
349-
stack-wide workload contract. Code owns agent permission and escalation policy,
350-
A3S Runtime owns provider-neutral Task and Service lifecycle and placement, and
351-
A3S Box owns OCI and stronger-isolation workloads. Replacing the CLI's
352-
transitional npm bootstrap with an A3S-signed sandbox artifact does not change
353-
the `BashSandbox` contract.
344+
Hosts use `from_verified_npm_with_node` when a lifecycle owner supplies exact
345+
SRT and Node paths. The verified constructor requires the expected npm package
346+
identity and a tested SRT version; `new` likewise requires an explicit path for
347+
an intentionally supplied custom adapter. The A3S CLI uses the verified
348+
exact-path form after validating and, when policy allows, preparing its
349+
user-wide managed installation; Core does not install host software.
350+
351+
This adapter is a low-latency local enforcement provider, not the stack-wide
352+
workload contract. Code owns agent permission and escalation policy, A3S
353+
Runtime owns provider-neutral Task and Service lifecycle and placement, and A3S
354+
Box owns OCI and stronger-isolation workloads. Replacing the CLI's transitional
355+
npm bootstrap with an A3S-signed sandbox artifact does not change the
356+
`BashSandbox` contract.
354357

355358
Long-running hosts can call `AgentSession::add_skill`, `remove_skill`, and
356359
`skill_names` without rebuilding the session. The model-visible Skill catalog

core/Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "a3s-code-core"
3-
version = "6.1.0"
3+
version = "6.3.1"
44
edition = "2021"
55
authors = ["A3S Lab Team"]
66
license = "MIT"
@@ -122,6 +122,9 @@ aws-smithy-runtime-api = { version = "1", optional = true }
122122
[target.'cfg(unix)'.dependencies]
123123
libc = "0.2"
124124

125+
[target.'cfg(windows)'.dependencies]
126+
windows-sys = { version = "0.61", features = ["Win32_Storage_FileSystem"] }
127+
125128
[features]
126129
default = []
127130
# Enable OpenTelemetry OTLP export for traces and metrics.

core/src/agent_api/tests.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4342,7 +4342,7 @@ async fn test_resume_session_restores_verification_reports() {
43424342
}
43434343

43444344
#[tokio::test(flavor = "multi_thread")]
4345-
async fn test_verify_commands_builds_report_from_bash_results() {
4345+
async fn test_verify_commands_builds_report_from_shell_results() {
43464346
let temp_dir = tempfile::tempdir().unwrap();
43474347
let agent = Agent::from_config(test_config()).await.unwrap();
43484348
let session = agent
@@ -4354,7 +4354,7 @@ async fn test_verify_commands_builds_report_from_bash_results() {
43544354
"check:smoke",
43554355
"smoke",
43564356
"Run smoke command",
4357-
"printf ok",
4357+
"echo ok",
43584358
),
43594359
crate::verification::VerificationCommand::required(
43604360
"check:failure",

core/src/code_intelligence/language_runtime/tests.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,11 @@ use super::super::{
77
document_store::DocumentObservationKind, lsp::initialize::ServerTextSyncMode, CodePosition,
88
CodeRange, CodeSymbolKind, DocumentSymbol,
99
};
10+
#[cfg(unix)]
11+
use super::paths::existing_workspace_file_uri;
1012
use super::{
1113
bound_document_symbols,
12-
paths::{
13-
existing_workspace_file_uri, valid_utf16_position, validate_canonical_root,
14-
validate_workspace_path,
15-
},
14+
paths::{valid_utf16_position, validate_canonical_root, validate_workspace_path},
1615
protocol::{bound_items, did_save_params, document_sync_plan, DocumentSyncStep},
1716
MAX_DOCUMENT_SYMBOLS,
1817
};

core/src/code_intelligence/lsp/process.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,9 @@ mod tests {
329329
use super::*;
330330
use crate::code_intelligence::lsp::router::{ServerRequestRouter, ServerRequestRouterConfig};
331331
use std::collections::BTreeMap;
332+
#[cfg(unix)]
332333
use std::ffi::OsString;
334+
#[cfg(unix)]
333335
use std::time::Instant;
334336

335337
#[test]

core/src/code_intelligence/workspace_runtime/integration_tests.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,7 +487,9 @@ async fn forced_shutdown_reaps_an_unresponsive_language_runtime() {
487487
};
488488
let process_state = language_runtime.subscribe_process_state();
489489
let log_path = server.with_extension("log");
490+
#[cfg(unix)]
490491
let log = std::fs::read_to_string(&log_path).unwrap();
492+
#[cfg(unix)]
491493
let pid = *fixture_started_pids(&log).last().unwrap();
492494

493495
tokio::time::timeout(Duration::from_secs(6), runtime.shutdown())

core/src/llm/openai/tests.rs

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,179 @@ async fn streaming_parser_closes_when_caller_cancels() {
204204
assert!(next.is_none());
205205
}
206206

207+
#[tokio::test]
208+
async fn streaming_transport_error_after_partial_delta_does_not_emit_done() {
209+
use crate::llm::{LlmClient, StreamEvent};
210+
211+
let client = OpenAiClient::new("k".to_string(), "model".to_string()).with_http_client(
212+
std::sync::Arc::new(FailingSseHttp {
213+
chunks: vec![
214+
"data: {\"choices\":[{\"delta\":{\"content\":\"partial\"}}]}\n\n".to_string(),
215+
],
216+
}),
217+
);
218+
let mut rx = client
219+
.complete_streaming(
220+
&[Message::user("go")],
221+
None,
222+
&[],
223+
tokio_util::sync::CancellationToken::new(),
224+
)
225+
.await
226+
.expect("stream opened");
227+
228+
let mut text = String::new();
229+
let mut saw_done = false;
230+
while let Some(event) = rx.recv().await {
231+
match event {
232+
StreamEvent::TextDelta(delta) => text.push_str(&delta),
233+
StreamEvent::Done(_) => saw_done = true,
234+
_ => {}
235+
}
236+
}
237+
238+
assert_eq!(text, "partial");
239+
assert!(
240+
!saw_done,
241+
"a failed transport must close without Done so the agent retries the turn"
242+
);
243+
}
244+
245+
#[tokio::test]
246+
async fn streaming_clean_eof_after_partial_delta_does_not_emit_done() {
247+
use crate::llm::{LlmClient, StreamEvent};
248+
249+
let client = glm_client(vec![
250+
"data: {\"choices\":[{\"delta\":{\"content\":\"partial\"}}]}\n\n".to_string(),
251+
]);
252+
let mut rx = client
253+
.complete_streaming(
254+
&[Message::user("go")],
255+
None,
256+
&[],
257+
tokio_util::sync::CancellationToken::new(),
258+
)
259+
.await
260+
.expect("stream opened");
261+
262+
let mut text = String::new();
263+
let mut saw_done = false;
264+
while let Some(event) = rx.recv().await {
265+
match event {
266+
StreamEvent::TextDelta(delta) => text.push_str(&delta),
267+
StreamEvent::Done(_) => saw_done = true,
268+
_ => {}
269+
}
270+
}
271+
272+
assert_eq!(text, "partial");
273+
assert!(
274+
!saw_done,
275+
"EOF without protocol terminal evidence must close without Done"
276+
);
277+
}
278+
279+
#[tokio::test]
280+
async fn streaming_transport_error_after_finish_reason_can_finalize() {
281+
let client = OpenAiClient::new("k".to_string(), "model".to_string()).with_http_client(
282+
std::sync::Arc::new(FailingSseHttp {
283+
chunks: vec![
284+
"data: {\"choices\":[{\"delta\":{\"content\":\"complete\"}}]}\n\n".to_string(),
285+
"data: {\"choices\":[{\"delta\":{},\"finish_reason\":\"stop\"}]}\n\n".to_string(),
286+
],
287+
}),
288+
);
289+
290+
let response = drain_to_done(&client).await;
291+
assert_eq!(response.text(), "complete");
292+
assert_eq!(response.stop_reason.as_deref(), Some("stop"));
293+
}
294+
295+
#[tokio::test]
296+
async fn streaming_clean_eof_after_finish_reason_can_finalize() {
297+
let client = glm_client(vec![
298+
"data: {\"choices\":[{\"delta\":{\"content\":\"complete\"}}]}\n\n".to_string(),
299+
"data: {\"choices\":[{\"delta\":{},\"finish_reason\":\"stop\"}]}\n\n".to_string(),
300+
]);
301+
302+
let response = drain_to_done(&client).await;
303+
assert_eq!(response.text(), "complete");
304+
assert_eq!(response.stop_reason.as_deref(), Some("stop"));
305+
}
306+
307+
#[tokio::test]
308+
async fn streaming_partial_response_is_not_finalized_after_cancellation() {
309+
use crate::llm::{LlmClient, StreamEvent};
310+
311+
let client = OpenAiClient::new("k".to_string(), "model".to_string()).with_http_client(
312+
std::sync::Arc::new(ChunksThenPendingSseHttp {
313+
chunks: vec![
314+
"data: {\"choices\":[{\"delta\":{\"content\":\"partial\"}}]}\n\n".to_string(),
315+
],
316+
}),
317+
);
318+
let cancellation = tokio_util::sync::CancellationToken::new();
319+
let mut rx = client
320+
.complete_streaming(&[Message::user("go")], None, &[], cancellation.clone())
321+
.await
322+
.expect("stream opened");
323+
324+
assert!(matches!(
325+
rx.recv().await,
326+
Some(StreamEvent::TextDelta(text)) if text == "partial"
327+
));
328+
cancellation.cancel();
329+
330+
let next = tokio::time::timeout(std::time::Duration::from_millis(100), rx.recv())
331+
.await
332+
.expect("provider parser must stop after cancellation");
333+
assert!(next.is_none(), "cancellation must not synthesize Done");
334+
}
335+
336+
#[tokio::test]
337+
async fn streaming_done_closes_before_pending_transport_and_emits_once() {
338+
use crate::llm::{LlmClient, StreamEvent};
339+
340+
let client = OpenAiClient::new("k".to_string(), "model".to_string()).with_http_client(
341+
std::sync::Arc::new(ChunksThenPendingSseHttp {
342+
chunks: vec![
343+
"data: {\"choices\":[{\"delta\":{\"content\":\"complete\"}}]}\n\n".to_string(),
344+
"data: [DONE]\n\n".to_string(),
345+
"data: [DONE]\n\n".to_string(),
346+
],
347+
}),
348+
);
349+
let mut rx = client
350+
.complete_streaming(
351+
&[Message::user("go")],
352+
None,
353+
&[],
354+
tokio_util::sync::CancellationToken::new(),
355+
)
356+
.await
357+
.expect("stream opened");
358+
359+
let events = tokio::time::timeout(std::time::Duration::from_secs(1), async move {
360+
let mut events = Vec::new();
361+
while let Some(event) = rx.recv().await {
362+
events.push(event);
363+
}
364+
events
365+
})
366+
.await
367+
.expect("[DONE] must close the parser without waiting for transport EOF");
368+
let done = events
369+
.into_iter()
370+
.filter_map(|event| match event {
371+
StreamEvent::Done(response) => Some(response),
372+
_ => None,
373+
})
374+
.collect::<Vec<_>>();
375+
376+
assert_eq!(done.len(), 1, "[DONE] must emit exactly one final response");
377+
assert_eq!(done[0].text(), "complete");
378+
}
379+
207380
#[tokio::test]
208381
async fn streaming_parser_preserves_unicode_split_across_transport_chunks() {
209382
let wire = concat!(

0 commit comments

Comments
 (0)