Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
2b1234a
feat(imagepolicy): add target policies and CGO-aware semantic options
antoinetoussaint-byte Oct 27, 2026
b9f41ae
chore: sync merged cve branch with main
antoinetoussaint-byte Oct 28, 2026
4e23da7
fix(companions): refresh runtime bases and qualify unpublished compan…
antoinetoussaint-byte Oct 28, 2026
5c2bc78
fix(proto): preserve generator compatibility on the refreshed runtime
antoinetoussaint-byte Oct 28, 2026
1e81052
Merge remote-tracking branch 'origin/main' into cve
antoinetoussaint-byte Oct 28, 2026
cdb7f45
fix(deps): refresh vulnerable Go dependencies and companion versions
antoinetoussaint-byte Oct 28, 2026
8f998a3
fix(deps): retain the Kubernetes module compatibility pins
antoinetoussaint-byte Oct 28, 2026
702bac2
Merge remote-tracking branch 'origin/main' into cve
antoinetoussaint-byte Oct 28, 2026
96fc6d0
fix(proto): remove Rust generation and companion build tooling
antoinetoussaint-byte Oct 28, 2026
09b1811
test(proto): remove Rust from generation and base inventories
antoinetoussaint-byte Oct 28, 2026
26fea99
fix(proto): keep the Swagger companion alive during generation
antoinetoussaint-byte Oct 28, 2026
44f996f
Merge remote-tracking branch 'origin/main' into cve
antoinetoussaint-byte Oct 28, 2026
f1e8e83
fix(proto): cross-compile tools without target emulation
antoinetoussaint-byte Oct 28, 2026
4df9af6
test(proto): execute target tools in each architecture build
antoinetoussaint-byte Oct 28, 2026
e0528b1
Merge remote-tracking branch 'origin/main' into cve
antoinetoussaint-byte Oct 28, 2026
aa60b35
fix(proto): keep runtime caches writable for host users
antoinetoussaint-byte Oct 28, 2026
fdb6025
docs(proto): keep generator source citation current
antoinetoussaint-byte Oct 28, 2026
8a28370
test(proto): compile a real schema in non-root cache checks
antoinetoussaint-byte Oct 28, 2026
b43d4f9
Merge remote-tracking branch 'origin/main' into cve
antoinetoussaint-byte Oct 28, 2026
b558c86
fix(python): remove build-only pip and bootstrap vendors
antoinetoussaint-byte Oct 28, 2026
a262446
fix(dap): configure deferred launch and attach before awaiting responses
antoinetoussaint-byte Oct 28, 2026
0ffe2e6
fix(dap): omit unspecified launch arguments and environment
antoinetoussaint-byte Oct 28, 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
58 changes: 50 additions & 8 deletions companions/dap/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,27 +164,69 @@ func (c *companionClient) configurationDone(ctx context.Context) error {
func (c *companionClient) Launch(ctx context.Context, program string, args []string, env map[string]string) error {
launchArgs := map[string]interface{}{
"program": program,
"args": args,
"env": env,
"cwd": "/workspace",
"stopOnEntry": false,
}
_, err := c.tp.request(ctx, "launch", launchArgs)
if err != nil {
return err
// Nil means no override. JSON null is not a valid argument array or
// environment object for adapters such as debugpy.
if args != nil {
launchArgs["args"] = args
}
return c.configurationDone(ctx)
if env != nil {
launchArgs["env"] = env
}
return c.startDebugSession(ctx, "launch", launchArgs)
}

func (c *companionClient) Attach(ctx context.Context, pid int) error {
attachArgs := map[string]interface{}{
"processId": pid,
}
_, err := c.tp.request(ctx, "attach", attachArgs)
return c.startDebugSession(ctx, "attach", attachArgs)
}

// Adapters may defer the launch/attach response until configuration is done.
// Send the start request first, wait for the adapter's initialized event, then
// complete configuration before awaiting the start response.
func (c *companionClient) startDebugSession(ctx context.Context, command string, args map[string]interface{}) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
wait, err := c.tp.sendRequest(command, args)
if err != nil {
return err
}
return c.configurationDone(ctx)
result := make(chan error, 1)
go func() {
_, err := wait(ctx)
result <- err
}()
var response <-chan error = result
for {
select {
case err := <-response:
if err != nil {
return err
}
response = nil // Early success still requires initialized/configurationDone.
case <-c.tp.initialized:
if err := c.configurationDone(ctx); err != nil {
return err
}
if response == nil {
return nil
}
select {
case err := <-response:
return err
case <-ctx.Done():
return ctx.Err()
}
case <-c.tp.done:
return fmt.Errorf("%s: transport closed before configuration completed", command)
case <-ctx.Done():
return ctx.Err()
}
}
}

func (c *companionClient) SetBreakpoints(ctx context.Context, file string, lines []int) ([]BreakpointResult, error) {
Expand Down
134 changes: 134 additions & 0 deletions companions/dap/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package dap

import (
"bufio"
"context"
"encoding/json"
"fmt"
"io"
"net"
"strconv"
"testing"
"time"

"github.com/stretchr/testify/require"
)

// These wire exchanges exercise the maintained client and transport without
// starting an adapter, a companion, or a host service. The real debugpy test
// remains separately required under python_companion_required.
func TestStartDebugSessionProtocol(t *testing.T) {
for _, command := range []string{"launch", "attach"} {
for _, behavior := range []string{"deferred", "early", "rejected", "configuration-error", "disconnect", "cancel"} {
t.Run(command+"/"+behavior, func(t *testing.T) {
var args []string
var env map[string]string
if behavior == "early" {
args = []string{"--example", "value"}
env = map[string]string{"EXAMPLE": "value"}
}
ctx, cancel := context.WithTimeout(t.Context(), 3*time.Second)
defer cancel()
conn, peer := net.Pipe()
t.Cleanup(func() { _ = conn.Close(); _ = peer.Close() })
client := &companionClient{tp: newTransport(ctx, conn)}
peerDone := make(chan struct{})
go func() {
defer close(peerDone)
reader := bufio.NewReader(peer)
start := readProtocolRequest(t, reader)
require.Equal(t, command, start.Command)
if command == "launch" {
wire := start.Arguments.(map[string]interface{})
t.Logf("launch wire arguments: %v", wire)
if args == nil {
require.NotContains(t, wire, "args")
require.NotContains(t, wire, "env")
} else {
require.Equal(t, []interface{}{"--example", "value"}, wire["args"])
require.Equal(t, map[string]interface{}{"EXAMPLE": "value"}, wire["env"])
}
}
if behavior == "cancel" {
cancel()
return
}
if behavior == "disconnect" {
_ = peer.Close()
return
}
if behavior == "rejected" {
writeProtocolMessage(t, peer, dapResponse{Type: "response", RequestSeq: start.Seq, Success: false, Message: "start refused"})
return
}
if behavior == "early" {
writeProtocolMessage(t, peer, dapResponse{Type: "response", RequestSeq: start.Seq, Success: true})
}
writeProtocolMessage(t, peer, dapEvent{Type: "event", Event: "initialized"})
configuration := readProtocolRequest(t, reader)
require.Equal(t, "configurationDone", configuration.Command)
writeProtocolMessage(t, peer, dapResponse{Type: "response", RequestSeq: configuration.Seq,
Success: behavior != "configuration-error", Message: "configuration refused"})
if behavior != "early" && behavior != "configuration-error" {
writeProtocolMessage(t, peer, dapResponse{Type: "response", RequestSeq: start.Seq, Success: true})
}
}()
var err error
if command == "launch" {
err = client.Launch(ctx, "/workspace/main.py", args, env)
} else {
err = client.Attach(ctx, 42)
}
switch behavior {
case "deferred", "early":
require.NoError(t, err)
case "rejected":
require.ErrorContains(t, err, "start refused")
case "configuration-error":
require.ErrorContains(t, err, "configuration refused")
default:
require.Error(t, err)
}
<-peerDone
})
}
}
}

func TestConnectionProbePreservesStartupEvent(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), time.Second)
defer cancel()
conn, peer := net.Pipe()
t.Cleanup(func() { _ = conn.Close(); _ = peer.Close() })
go writeProtocolMessage(t, peer, dapEvent{Type: "event", Event: "initialized"})
ready, err := probeConnection(conn)
require.NoError(t, err)
transport := newTransport(ctx, ready)
select {
case <-transport.initialized:
case <-ctx.Done():
t.Fatal("connection probe discarded the first byte of the startup DAP event")
}
}

func readProtocolRequest(t *testing.T, reader *bufio.Reader) dapRequest {
t.Helper()
headers, err := (&transport{reader: reader}).readHeader()
require.NoError(t, err)
length, err := strconv.Atoi(headers["Content-Length"])
require.NoError(t, err)
body := make([]byte, length)
_, err = io.ReadFull(reader, body)
require.NoError(t, err)
var request dapRequest
require.NoError(t, json.Unmarshal(body, &request))
return request
}

func writeProtocolMessage(t *testing.T, writer io.Writer, message interface{}) {
t.Helper()
body, err := json.Marshal(message)
require.NoError(t, err)
_, err = fmt.Fprintf(writer, "Content-Length: %d\r\n\r\n%s", len(body), body)
require.NoError(t, err)
}
56 changes: 56 additions & 0 deletions companions/dap/python_integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//go:build python_companion_required

package dap

import (
"context"
"os"
"os/exec"
"path/filepath"
"testing"
"time"

"github.com/codefly-dev/core/companions/python"
"github.com/codefly-dev/core/companions/testutil"
"github.com/codefly-dev/core/languages"
"github.com/codefly-dev/core/runners/companion"
"github.com/stretchr/testify/require"
)

func TestPythonCompanionInitializesAndLaunchesWithoutPip(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), time.Minute)
defer cancel()
testutil.RequireDocker(t, ctx)
img, err := python.CompanionImage(ctx)
require.NoError(t, err)
out, err := exec.CommandContext(ctx, "docker", "image", "inspect", img.FullName()).CombinedOutput()
require.NoError(t, err, "build the selected companion with codefly companion build python: %s", out)
source := t.TempDir()
program, err := os.ReadFile("testdata/python/main.py")
require.NoError(t, err)
require.NoError(t, os.WriteFile(filepath.Join(source, "main.py"), program, 0o644))
client, err := NewClient(ctx, languages.PYTHON, source)
require.NoError(t, err)
t.Cleanup(func() {
cleanup, stop := context.WithTimeout(context.Background(), 15*time.Second)
defer stop()
require.NoError(t, client.Close(cleanup))
})
require.Equal(t, companion.BackendDocker, client.(*companionClient).runner.Backend())
terminated := make(chan struct{}, 1)
client.OnTerminated(func() {
select {
case terminated <- struct{}{}:
default:
}
})
require.NoError(t, client.Launch(ctx, "/workspace/main.py", nil, nil))
select {
case <-terminated:
case <-ctx.Done():
t.Fatal(ctx.Err())
}
result, err := os.ReadFile(filepath.Join(source, "result.txt"))
require.NoError(t, err)
require.Equal(t, "debugpy without pip", string(result))
}
8 changes: 8 additions & 0 deletions companions/dap/testdata/python/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import importlib.util
from pathlib import Path

import debugpy

assert importlib.util.find_spec("pip") is None
assert importlib.util.find_spec("ensurepip") is None
Path(__file__).with_name("result.txt").write_text("debugpy without pip")
Loading
Loading