Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
4 changes: 2 additions & 2 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,14 @@ jobs:
go-version: '1.22'

- name: golangci-lint (clients/go)
uses: golangci/golangci-lint-action@v6
uses: golangci/golangci-lint-action@v9
with:
version: latest
working-directory: clients/go
args: --timeout=5m

- name: golangci-lint (gateway)
uses: golangci/golangci-lint-action@v6
uses: golangci/golangci-lint-action@v9
with:
version: latest
working-directory: gateway
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ pnpm-debug.log*
dist/
build/
.next/
.next-build/
.next-playwright-*/
out/
.turbo/
.vercel/
Expand Down
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Changelog

All notable changes to this project are documented in this file.

## [Unreleased]

### Added

- Add authenticated Streamable HTTP MCP with scoped read and write tools.
- Add OAuth 2.1 with PKCE, dynamic client registration, and browser OIDC.
- Add personal API tokens with scopes, expiry, one-time display, and revocation.
- Add OpenAPI JSON and YAML documents, `llms.txt`, and client authentication guidance.
- Add batch append and blob reads, bounded trace pages, exact turn hydration, and faster typed projection.
- Add responsive token management and debugger views for mobile devices.

### Changed

- Enforce write scopes and stricter proxy-header handling.
- Correct separate tool-result hydration and named-key MessagePack projection. Numeric field tags continue to have priority.

### Fixed

- Pin pnpm 9 in Node 20 container builds.

### Compatibility

- This change has no known breaking API or stored-data changes.
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ FROM node:20-alpine AS frontend
WORKDIR /app

# Install pnpm
RUN corepack enable && corepack prepare pnpm@latest --activate
RUN corepack enable && corepack prepare pnpm@9.15.9 --activate

# Copy package files
COPY frontend/package.json frontend/pnpm-lock.yaml* ./
Expand All @@ -28,7 +28,7 @@ RUN pnpm build
# ============================================
# Stage 2: Build Rust binary
# ============================================
FROM rust:1.92-bookworm AS backend
FROM rust:1.94.1-bookworm AS backend

WORKDIR /app

Expand Down
12 changes: 6 additions & 6 deletions clients/go/cmd/cxdb-blob-verify/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ func main() {
// --- Test 1: PutBlob + GetBlob round-trip ---
run("PutBlob + GetBlob round-trip", func() error {
client := dial()
defer client.Close()
defer func() { _ = client.Close() }()

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
Expand Down Expand Up @@ -100,7 +100,7 @@ func main() {
// --- Test 2: GetBlob nonexistent hash returns ErrBlobNotFound ---
run("GetBlob nonexistent hash (expect ErrBlobNotFound)", func() error {
client := dial()
defer client.Close()
defer func() { _ = client.Close() }()

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
Expand All @@ -123,7 +123,7 @@ func main() {
// --- Test 3: PutBlobIfAbsent deduplication ---
run("PutBlobIfAbsent deduplication", func() error {
client := dial()
defer client.Close()
defer func() { _ = client.Close() }()

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
Expand Down Expand Up @@ -166,7 +166,7 @@ func main() {
// --- Test 4: Large blob (1 MiB) ---
run("Large blob (1 MiB) round-trip", func() error {
client := dial()
defer client.Close()
defer func() { _ = client.Close() }()

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
Expand Down Expand Up @@ -196,7 +196,7 @@ func main() {
// --- Test 5: Connection survives a not-found ---
run("Connection survives not-found", func() error {
client := dial()
defer client.Close()
defer func() { _ = client.Close() }()

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
Expand Down Expand Up @@ -244,7 +244,7 @@ func main() {
if err != nil {
return fmt.Errorf("DialReconnecting: %w", err)
}
defer rc.Close()
defer func() { _ = rc.Close() }()

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
Expand Down
14 changes: 7 additions & 7 deletions clients/go/fs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func mockServer(t *testing.T, handler mockHandler) (addr string, cleanup func())
if err != nil {
return // listener closed
}
defer conn.Close()
defer func() { _ = conn.Close() }()

// --- HELLO handshake ---
helloFrame, err := mockReadFrame(conn)
Expand Down Expand Up @@ -107,7 +107,7 @@ func mockServer(t *testing.T, handler mockHandler) (addr string, cleanup func())
}()

cleanup = func() {
ln.Close()
_ = ln.Close()
wg.Wait()
}
t.Cleanup(cleanup)
Expand Down Expand Up @@ -180,7 +180,7 @@ func TestGetBlob_Success(t *testing.T) {
if err != nil {
t.Fatalf("Dial: %v", err)
}
defer client.Close()
defer func() { _ = client.Close() }()

data, err := client.GetBlob(context.Background(), requestHash)
if err != nil {
Expand All @@ -200,7 +200,7 @@ func TestGetBlob_NotFound(t *testing.T) {
if err != nil {
t.Fatalf("Dial: %v", err)
}
defer client.Close()
defer func() { _ = client.Close() }()

var hash [32]byte
_, err = client.GetBlob(context.Background(), hash)
Expand All @@ -222,7 +222,7 @@ func TestGetBlob_ResponseTooShort(t *testing.T) {
if err != nil {
t.Fatalf("Dial: %v", err)
}
defer client.Close()
defer func() { _ = client.Close() }()

var hash [32]byte
_, err = client.GetBlob(context.Background(), hash)
Expand All @@ -247,7 +247,7 @@ func TestGetBlob_PayloadTruncated(t *testing.T) {
if err != nil {
t.Fatalf("Dial: %v", err)
}
defer client.Close()
defer func() { _ = client.Close() }()

var hash [32]byte
_, err = client.GetBlob(context.Background(), hash)
Expand Down Expand Up @@ -323,7 +323,7 @@ func TestPutBlobThenGetBlob_Roundtrip(t *testing.T) {
if err != nil {
t.Fatalf("Dial: %v", err)
}
defer client.Close()
defer func() { _ = client.Close() }()

blobData := []byte("the quick brown fox jumps over the lazy dog")

Expand Down
18 changes: 9 additions & 9 deletions clients/go/fstree/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func TestE2E_FilesystemSnapshots(t *testing.T) {
if err != nil {
t.Fatalf("Failed to connect to server at %s: %v\nMake sure the server is running", binaryAddr, err)
}
defer client.Close()
defer func() { _ = client.Close() }()

t.Logf("Connected to CXDB server, session ID: %d", client.SessionID())

Expand Down Expand Up @@ -483,9 +483,9 @@ func makePayload(t *testing.T, itemType, text string) []byte {
t.Helper()

item := map[uint64]any{
1: itemType, // type
2: "complete", // status
3: time.Now().UnixMilli(), // timestamp
1: itemType, // type
2: "complete", // status
3: time.Now().UnixMilli(), // timestamp
4: fmt.Sprintf("test-%d", time.Now().UnixNano()), // id
}

Expand Down Expand Up @@ -520,7 +520,7 @@ func verifyHTTPFsListing(t *testing.T, turnID uint64, path string, expectedNames
if err != nil {
t.Fatalf("HTTP GET %s failed: %v", url, err)
}
defer resp.Body.Close()
defer func() { _ = resp.Body.Close() }()

if resp.StatusCode != 200 {
body, _ := io.ReadAll(resp.Body)
Expand Down Expand Up @@ -558,7 +558,7 @@ func verifyHTTPFsFileContent(t *testing.T, turnID uint64, path string, expectedC
if err != nil {
t.Fatalf("HTTP GET %s failed: %v", url, err)
}
defer resp.Body.Close()
defer func() { _ = resp.Body.Close() }()

if resp.StatusCode != 200 {
body, _ := io.ReadAll(resp.Body)
Expand Down Expand Up @@ -591,7 +591,7 @@ func verifyHTTPFsFileNotFound(t *testing.T, turnID uint64, path string) {
if err != nil {
t.Fatalf("HTTP GET %s failed: %v", url, err)
}
defer resp.Body.Close()
defer func() { _ = resp.Body.Close() }()

if resp.StatusCode != 404 {
t.Errorf("Expected 404 for turn %d path '%s', got %d", turnID, path, resp.StatusCode)
Expand All @@ -615,7 +615,7 @@ func TestE2E_BlobDeduplication(t *testing.T) {
if err != nil {
t.Fatalf("Failed to connect: %v", err)
}
defer client.Close()
defer func() { _ = client.Close() }()

// Create unique content
uniqueContent := []byte(fmt.Sprintf("unique content %d", time.Now().UnixNano()))
Expand Down Expand Up @@ -655,7 +655,7 @@ func TestE2E_FsRootInheritance(t *testing.T) {
if err != nil {
t.Fatalf("Failed to connect: %v", err)
}
defer client.Close()
defer func() { _ = client.Close() }()

workDir := t.TempDir()
os.WriteFile(filepath.Join(workDir, "test.txt"), []byte("inherited content"), 0644)
Expand Down
12 changes: 11 additions & 1 deletion clients/rust/src/follow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,16 @@ mod tests {
follow_turns(&ctx, event_rx, client.clone(), vec![with_follow_buffer(10)]);

event_tx.send(make_turn_event(context_id, 2, 1)).unwrap();
let mut got = vec![
out.recv_timeout(Duration::from_secs(1))
.expect("backfill turn 1")
.turn
.turn_id,
out.recv_timeout(Duration::from_secs(1))
.expect("backfill turn 2")
.turn
.turn_id,
];

client.set_context(
context_id,
Expand Down Expand Up @@ -495,7 +505,7 @@ mod tests {
event_tx.send(make_turn_event(context_id, 3, 2)).unwrap();
drop(event_tx);

let got: Vec<u64> = out.iter().map(|turn| turn.turn.turn_id).collect();
got.extend(out.iter().map(|turn| turn.turn.turn_id));
if let Some(err) = errs.try_iter().next() {
panic!("unexpected error: {}", err);
}
Expand Down
16 changes: 10 additions & 6 deletions cxtx/src/delivery.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright 2025 StrongDM Inc
// SPDX-License-Identifier: Apache-2.0

use anyhow::{anyhow, Result};
use std::collections::VecDeque;
use std::time::Duration;
Expand Down Expand Up @@ -28,7 +31,7 @@ enum WorkerMessage {
#[derive(Debug, Clone)]
enum QueueItem {
CreateContext,
Append(TurnEnvelope),
Append(Box<TurnEnvelope>),
}

impl DeliveryHandle {
Expand All @@ -54,7 +57,7 @@ impl DeliveryHandle {

pub async fn enqueue_turn(&self, turn: TurnEnvelope) -> Result<()> {
self.tx
.send(WorkerMessage::Enqueue(QueueItem::Append(turn)))
.send(WorkerMessage::Enqueue(QueueItem::Append(Box::new(turn))))
.await
.map_err(|_| anyhow!("delivery worker is no longer running"))
}
Expand Down Expand Up @@ -135,8 +138,9 @@ impl DeliveryWorker {

if self.degraded && self.queue.is_empty() && !self.recovery_turn_enqueued {
self.recovery_turn_enqueued = true;
self.queue
.push_back(QueueItem::Append(self.session.ingest_recovered_turn(0)));
self.queue.push_back(QueueItem::Append(Box::new(
self.session.ingest_recovered_turn(0),
)));
} else if self.degraded
&& self.recovery_turn_enqueued
&& matches!(item, QueueItem::Append(_))
Expand Down Expand Up @@ -223,9 +227,9 @@ impl DeliveryWorker {

self.degraded = true;
self.recovery_turn_enqueued = false;
self.queue.push_back(QueueItem::Append(
self.queue.push_back(QueueItem::Append(Box::new(
self.session.ingest_degraded_turn(self.queue.len(), error),
));
)));
eprintln!("cxtx: CXDB ingest unavailable, entering queued-delivery mode");
}

Expand Down
6 changes: 5 additions & 1 deletion cxtx/src/provider/anthropic.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright 2025 StrongDM Inc
// SPDX-License-Identifier: Apache-2.0

use serde_json::Value;
use std::collections::BTreeMap;

Expand Down Expand Up @@ -425,7 +428,8 @@ fn is_bootstrap_user_block(block: &Value) -> bool {
text.starts_with("<system-reminder>")
&& (text.contains("SessionStart hook additional context")
|| text.contains("The following skills are available for use with the Skill tool:")
|| text.contains("As you answer the user's questions, you can use the following context:"))
|| text
.contains("As you answer the user's questions, you can use the following context:"))
}

fn parse_assistant_content(
Expand Down
8 changes: 4 additions & 4 deletions cxtx/src/provider/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright 2025 StrongDM Inc
// SPDX-License-Identifier: Apache-2.0

pub mod anthropic;
pub mod openai;

Expand Down Expand Up @@ -69,10 +72,7 @@ impl ProviderKind {
out.extend(["-c".to_string(), "prefer_websockets=false".to_string()]);
}
if !has_codex_feature_override(args, "responses_websockets") {
out.extend([
"--disable".to_string(),
"responses_websockets".to_string(),
]);
out.extend(["--disable".to_string(), "responses_websockets".to_string()]);
}
if !has_codex_feature_override(args, "responses_websockets_v2") {
out.extend([
Expand Down
Loading
Loading