feat: CLI profile proto message support

- Add runtime proto for CLI profile messages
- Update edge transport server with profile support
- Update node adapters factory for profile handling
- Regenerate protobuf code
This commit is contained in:
toki 2026-05-04 22:54:23 +09:00
parent 86ceabadc3
commit f873e27a15
9 changed files with 950 additions and 569 deletions

View file

@ -1,74 +0,0 @@
<!-- task=07_cli_profile_proto_message plan=0 tag=API -->
# Code Review Reference - API
## 개요
date=2026-05-04
task=07_cli_profile_proto_message, plan=0, tag=API
## 이 파일을 읽는 리뷰 에이전트에게
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료 후 반드시 아래 순서로 아카이브하세요.
1. `CODE_REVIEW-cloud-G10.md``code_review_cloud_G10_N.log` (N = 기존 code_review_*.log 수)
2. `PLAN-cloud-G09.md``plan_cloud_G09_M.log` (M = 기존 plan_*.log 수)
3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 routed plan + review 스텁 작성.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [API-1] proto 메시지 정의 및 생성물 갱신 | [ ] |
| [API-2] edge buildConfigPayload typed 메시지 갱신 | [ ] |
| [API-3] node BuildFromPayload typed 메시지 갱신 | [ ] |
## 계획 대비 변경 사항
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
## 주요 설계 결정
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
## 리뷰어를 위한 체크포인트
- `proto/iop/runtime.proto``oneof config`가 추가되고 기존 `settings` 필드가 backward-compat을 위해 유지되었는지
- `proto/gen/iop/runtime.pb.go`가 사람이 직접 편집되지 않고 `make proto`로 생성된 변경인지
- edge `buildConfigPayload`가 cli/ollama/vllm 모두 oneof로 채우는지, 더이상 `stringsToAny` 등 우회 헬퍼가 남아있지 않은지
- node `BuildFromPayload`에서 `cliConfFromStruct`/`boolFromAny`/`intFromAny`가 모두 제거되었는지
- Persistent/Terminal/Idle timeout 정수 필드가 손실 없이 전달되는 단위 테스트가 추가되었는지
## 검증 결과
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
### API-1 중간 검증
```
$ make proto
$ go build ./proto/gen/...
(output)
```
### API-2 중간 검증
```
$ go test ./apps/edge/internal/transport/...
(output)
```
### API-3 중간 검증
```
$ go test ./apps/node/internal/adapters/...
(output)
```
### 최종 검증
```
$ make proto
$ go build ./...
$ go test ./...
(output)
```

View file

@ -1,181 +0,0 @@
<!-- task=07_cli_profile_proto_message plan=0 tag=API -->
# CLI 프로파일 전용 proto 메시지 도입
## 이 파일을 읽는 구현 에이전트에게
아래 체크리스트를 순서대로 완료하고, 각 항목의 중간 검증과 최종 검증을 실제로 실행하세요. 구현이 끝나면 `agent-task/07_cli_profile_proto_message/CODE_REVIEW-cloud-G10.md`의 모든 섹션을 실제 구현 내용과 명령 출력으로 채우세요. `CODE_REVIEW-cloud-G10.md``이 파일을 읽는 리뷰 에이전트에게` 섹션에 있는 아카이브 지시(`*.log`로 이름 변경, `complete.log` 작성)는 구현 에이전트가 수행하면 안 되며, 리뷰 스킬 전용입니다.
## 배경
edge가 node에 보내는 `NodeConfigPayload`에서 CLI 프로파일은 `AdapterConfig.settings` (`google.protobuf.Struct`)로 직렬화되고, node 쪽에서 손으로 디코드합니다. (`apps/edge/internal/transport/server.go:190-207`의 `buildConfigPayload``apps/node/internal/adapters/factory.go:69-118``cliConfFromStruct`) 프로파일에 새 필드를 추가하면 edge·node 양쪽을 손으로 맞춰야 하며, structpb 인코딩 특성상 정수가 float로 들어가는 등의 변환 코드가 필요합니다. domain rule(`agent-ops/rules/project/rules.md`의 "protobuf 계약 변경은 .proto부터")을 따라 CLI 프로파일 전용 proto 메시지를 정의하고, edge·node 양쪽에서 구조체 단위로 주고받게 합니다.
## 의존 관계 및 구현 순서
1. `[API-1]` `proto/iop/runtime.proto``CLIAdapterConfig` / `CLIProfileConfig` 메시지를 추가하고 `make proto`로 생성물 갱신.
2. `[API-2]` edge `buildConfigPayload`가 새 메시지로 직접 채우도록 변경.
3. `[API-3]` node `BuildFromPayload`가 structpb 디코드 대신 새 메시지를 그대로 사용.
### [API-1] proto 메시지 정의 및 생성물 갱신
#### 문제
`proto/iop/runtime.proto:96-100``AdapterConfig``google.protobuf.Struct settings`로 모든 어댑터 타입의 설정을 우회 직렬화합니다. CLI 프로파일은 필드가 9개로 늘어나 string-keyed map 직렬화의 위험이 큽니다.
#### 해결 방법
`proto/iop/runtime.proto``AdapterConfig``oneof config`를 도입해 어댑터 타입별 typed 메시지를 가질 수 있도록 합니다. CLI 전용 메시지는 다음과 같이 정의합니다.
```proto
message AdapterConfig {
string type = 1;
bool enabled = 2;
google.protobuf.Struct settings = 3; // 구버전 호환용; 신규 어댑터는 oneof 사용 권장
oneof config {
CLIAdapterConfig cli = 4;
OllamaAdapterConfig ollama = 5;
VllmAdapterConfig vllm = 6;
}
}
message CLIAdapterConfig {
map<string, CLIProfileConfig> profiles = 1;
}
message CLIProfileConfig {
string command = 1;
repeated string args = 2;
repeated string env = 3;
bool persistent = 4;
bool terminal = 5;
int32 response_idle_timeout_ms = 6;
int32 startup_idle_timeout_ms = 7;
string output_format = 8;
}
message OllamaAdapterConfig { string base_url = 1; }
message VllmAdapterConfig { string endpoint = 1; }
```
오래된 `settings` 필드는 일단 유지해 mock/기타 어댑터 호환을 깨지 않습니다. CLI/Ollama/Vllm은 신규 oneof 경로만 사용하도록 합니다.
#### 수정 파일 및 체크리스트
- [ ] `proto/iop/runtime.proto``CLIAdapterConfig`, `CLIProfileConfig`, `OllamaAdapterConfig`, `VllmAdapterConfig` 메시지를 추가한다.
- [ ] `AdapterConfig``oneof config { ... }`를 추가한다 (`settings` 필드는 그대로 둔다).
- [ ] `make proto`를 실행해 `proto/gen/iop/runtime.pb.go`를 갱신한다.
- [ ] 생성 파일은 직접 수정하지 않는다.
#### 테스트 작성
테스트는 `[API-2]`/`[API-3]`에서 통합 검증한다. proto 정의 단독으로는 별도 단위 테스트를 작성하지 않는다.
#### 중간 검증
```bash
make proto
git diff proto/iop/runtime.proto
go build ./proto/gen/...
```
예상 결과: `runtime.proto`에 새 메시지/oneof가 추가되어 있고, 생성물이 빌드된다.
### [API-2] edge `buildConfigPayload`를 typed 메시지로 갱신
#### 문제
`apps/edge/internal/transport/server.go:155-209``buildConfigPayload``structpb.NewStruct(map[string]any{...})`로 CLI/Ollama/Vllm 설정을 우회 직렬화합니다. `stringsToAny` 같은 보조 함수도 필요합니다.
#### 해결 방법
CLI/Ollama/Vllm은 `oneof config`로 직접 채우고, mock만 기존 `settings` 경로를 유지합니다.
```go
if rec.Adapters.CLI.Enabled {
profiles := make(map[string]*iop.CLIProfileConfig, len(rec.Adapters.CLI.Profiles))
for name, p := range rec.Adapters.CLI.Profiles {
profiles[name] = &iop.CLIProfileConfig{
Command: p.Command,
Args: append([]string(nil), p.Args...),
Env: append([]string(nil), p.Env...),
Persistent: p.Persistent,
Terminal: p.Terminal,
ResponseIdleTimeoutMs: int32(p.ResponseIdleTimeoutMS),
StartupIdleTimeoutMs: int32(p.StartupIdleTimeoutMS),
OutputFormat: p.OutputFormat,
}
}
payload.Adapters = append(payload.Adapters, &iop.AdapterConfig{
Type: "cli", Enabled: true,
Config: &iop.AdapterConfig_Cli{Cli: &iop.CLIAdapterConfig{Profiles: profiles}},
})
}
```
#### 수정 파일 및 체크리스트
- [ ] `apps/edge/internal/transport/server.go``buildConfigPayload`에서 cli/ollama/vllm 분기를 oneof 기반으로 다시 쓴다.
- [ ] `stringsToAny` 헬퍼는 더 이상 사용되지 않으면 제거한다.
- [ ] mock 어댑터 분기는 그대로 둔다.
#### 테스트 작성
`apps/edge/internal/transport/server_test.go``TestBuildConfigPayload_CLIOneof` (가칭)를 추가해 CLI 프로파일 한 개 정의에서 oneof 경로가 채워지고, settings 필드는 비어 있는지 단언한다.
#### 중간 검증
```bash
go test ./apps/edge/internal/transport/...
```
예상 결과: 신규 테스트 포함 PASS.
### [API-3] node `BuildFromPayload`를 typed 메시지 기반으로 갱신
#### 문제
`apps/node/internal/adapters/factory.go:69-118``cliConfFromStruct`가 structpb를 손으로 디코드하면서 `boolFromAny`, `intFromAny` 같은 헬퍼에 의존합니다. structpb의 number→float64 변환 등 함정이 많습니다.
#### 해결 방법
`AdapterConfig.GetCli()`가 nil이 아니면 typed 메시지 그대로 `config.CLIConf`로 매핑하고, structpb 경로(`cliConfFromStruct`)는 제거합니다. Ollama/Vllm도 동일하게 변경.
#### 수정 파일 및 체크리스트
- [ ] `apps/node/internal/adapters/factory.go``cli/ollama/vllm` 분기를 oneof 기반으로 재작성한다.
- [ ] `cliConfFromStruct`, `ollamaConfFromStruct`, `vllmConfFromStruct`, `boolFromAny`, `intFromAny`를 모두 제거한다.
- [ ] `apps/node/internal/adapters/factory_internal_test.go``factory_test.go`의 케이스를 typed 메시지 입력 기반으로 갱신한다.
#### 테스트 작성
`factory_test.go`의 기존 시나리오(여러 어댑터 enabled, CLI 프로파일 매핑)를 oneof 입력으로 다시 작성한다. 신규 케이스: `Persistent`, `ResponseIdleTimeoutMS`, `StartupIdleTimeoutMS`가 정확히 0이 아닌 값으로 전달되는지 단언.
#### 중간 검증
```bash
go test ./apps/node/internal/adapters/...
```
예상 결과: PASS.
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `proto/iop/runtime.proto` | API-1 |
| `proto/gen/iop/runtime.pb.go` (생성물) | API-1 |
| `apps/edge/internal/transport/server.go` | API-2 |
| `apps/edge/internal/transport/server_test.go` | API-2 |
| `apps/node/internal/adapters/factory.go` | API-3 |
| `apps/node/internal/adapters/factory_internal_test.go` | API-3 |
| `apps/node/internal/adapters/factory_test.go` | API-3 |
## 최종 검증
```bash
make proto
go build ./...
go test ./...
```
예상 결과: build/test 모두 PASS, 새 oneof 경로로 edge↔node가 동작.

View file

@ -13,6 +13,7 @@ import (
"google.golang.org/protobuf/types/known/structpb"
edgenode "iop/apps/edge/internal/node"
"iop/packages/config"
iop "iop/proto/gen/iop"
)
@ -163,63 +164,69 @@ func buildConfigPayload(rec *edgenode.NodeRecord) (*iop.NodeConfigPayload, error
WorkspaceRoot: rec.Runtime.WorkspaceRoot,
},
}
addAdapter := func(typ string, enabled bool, settings map[string]any) error {
if !enabled {
return nil
}
st, err := structpb.NewStruct(settings)
if err != nil {
return fmt.Errorf("buildConfigPayload: %s: %w", typ, err)
}
mockSettings, err := structpb.NewStruct(nil)
if err != nil {
return nil, fmt.Errorf("buildConfigPayload: mock: %w", err)
}
payload.Adapters = append(payload.Adapters, &iop.AdapterConfig{
Type: "mock",
Enabled: true,
Settings: mockSettings,
})
if rec.Adapters.Ollama.Enabled {
payload.Adapters = append(payload.Adapters, &iop.AdapterConfig{
Type: typ,
Enabled: true,
Settings: st,
Type: "ollama",
Enabled: true,
Config: &iop.AdapterConfig_Ollama{
Ollama: &iop.OllamaAdapterConfig{BaseUrl: rec.Adapters.Ollama.BaseURL},
},
})
return nil
}
if err := addAdapter("mock", true, nil); err != nil {
return nil, err
}
if err := addAdapter("ollama", rec.Adapters.Ollama.Enabled, map[string]any{
"base_url": rec.Adapters.Ollama.BaseURL,
}); err != nil {
return nil, err
}
if err := addAdapter("vllm", rec.Adapters.Vllm.Enabled, map[string]any{
"endpoint": rec.Adapters.Vllm.Endpoint,
}); err != nil {
return nil, err
if rec.Adapters.Vllm.Enabled {
payload.Adapters = append(payload.Adapters, &iop.AdapterConfig{
Type: "vllm",
Enabled: true,
Config: &iop.AdapterConfig_Vllm{
Vllm: &iop.VllmAdapterConfig{Endpoint: rec.Adapters.Vllm.Endpoint},
},
})
}
if rec.Adapters.CLI.Enabled {
profiles := make(map[string]any)
profiles := make(map[string]*iop.CLIProfileConfig, len(rec.Adapters.CLI.Profiles))
for name, p := range rec.Adapters.CLI.Profiles {
profiles[name] = map[string]any{
"command": p.Command,
"args": stringsToAny(p.Args),
"env": stringsToAny(p.Env),
"persistent": p.Persistent,
"terminal": p.Terminal,
"response_idle_timeout_ms": p.ResponseIdleTimeoutMS,
"startup_idle_timeout_ms": p.StartupIdleTimeoutMS,
"output_format": p.OutputFormat,
"completion_marker": map[string]any{
"line": p.CompletionMarker.Line,
"regex": p.CompletionMarker.Regex,
},
}
}
if err := addAdapter("cli", true, map[string]any{"profiles": profiles}); err != nil {
return nil, err
profiles[name] = cliProfileToProto(p)
}
payload.Adapters = append(payload.Adapters, &iop.AdapterConfig{
Type: "cli",
Enabled: true,
Config: &iop.AdapterConfig_Cli{
Cli: &iop.CLIAdapterConfig{Profiles: profiles},
},
})
}
return payload, nil
}
func stringsToAny(ss []string) []interface{} {
out := make([]interface{}, len(ss))
for i, s := range ss {
out[i] = s
func cliProfileToProto(p config.CLIProfileConf) *iop.CLIProfileConfig {
out := &iop.CLIProfileConfig{
Command: p.Command,
Args: append([]string(nil), p.Args...),
Env: append([]string(nil), p.Env...),
Persistent: p.Persistent,
Terminal: p.Terminal,
ResponseIdleTimeoutMs: int32(p.ResponseIdleTimeoutMS),
StartupIdleTimeoutMs: int32(p.StartupIdleTimeoutMS),
OutputFormat: p.OutputFormat,
Mode: p.Mode,
ResumeArgs: append([]string(nil), p.ResumeArgs...),
}
if !p.CompletionMarker.Empty() {
out.CompletionMarker = &iop.CLICompletionMarker{
Line: p.CompletionMarker.Line,
Regex: p.CompletionMarker.Regex,
}
}
return out
}

View file

@ -47,7 +47,16 @@ func TestEdgeParserMap_NodeCommandResponse(t *testing.T) {
}
}
func TestBuildConfigPayload_CLIArgsRoundtrip(t *testing.T) {
func findCLIAdapter(payload *iop.NodeConfigPayload) *iop.AdapterConfig {
for _, a := range payload.GetAdapters() {
if a.GetType() == "cli" {
return a
}
}
return nil
}
func TestBuildConfigPayload_CLIOneof(t *testing.T) {
rec := &edgenode.NodeRecord{
ID: "node-local",
Alias: "local",
@ -75,64 +84,48 @@ func TestBuildConfigPayload_CLIArgsRoundtrip(t *testing.T) {
t.Fatalf("build config payload: %v", err)
}
var cliSettings map[string]any
for _, adapter := range payload.GetAdapters() {
if adapter.GetType() == "cli" {
cliSettings = adapter.GetSettings().AsMap()
break
}
cli := findCLIAdapter(payload)
if cli == nil {
t.Fatal("expected cli adapter")
}
if cliSettings == nil {
t.Fatal("expected cli adapter settings")
if cli.GetSettings() != nil {
t.Fatalf("expected settings to be nil for cli adapter, got %v", cli.GetSettings())
}
profiles, ok := cliSettings["profiles"].(map[string]any)
cliCfg := cli.GetCli()
if cliCfg == nil {
t.Fatal("expected typed cli config")
}
prof, ok := cliCfg.GetProfiles()["codex"]
if !ok {
t.Fatalf("expected profiles map, got %T", cliSettings["profiles"])
t.Fatal("expected codex profile")
}
profile, ok := profiles["codex"].(map[string]any)
if !ok {
t.Fatalf("expected codex profile map, got %T", profiles["codex"])
if prof.GetCommand() != "codex" {
t.Fatalf("command: %q", prof.GetCommand())
}
if got := profile["command"]; got != "codex" {
t.Fatalf("expected command %q, got %v", "codex", got)
if got := prof.GetArgs(); len(got) != 2 || got[0] != "--foo" || got[1] != "--bar" {
t.Fatalf("args: %#v", got)
}
args, ok := profile["args"].([]any)
if !ok {
t.Fatalf("expected args []any, got %T", profile["args"])
if got := prof.GetEnv(); len(got) != 1 || got[0] != "KEY=val" {
t.Fatalf("env: %#v", got)
}
if len(args) != 2 || args[0] != "--foo" || args[1] != "--bar" {
t.Fatalf("unexpected args: %#v", args)
if !prof.GetPersistent() {
t.Fatal("persistent")
}
env, ok := profile["env"].([]any)
if !ok {
t.Fatalf("expected env []any, got %T", profile["env"])
if !prof.GetTerminal() {
t.Fatal("terminal")
}
if len(env) != 1 || env[0] != "KEY=val" {
t.Fatalf("unexpected env: %#v", env)
if prof.GetResponseIdleTimeoutMs() != 1500 {
t.Fatalf("response_idle_timeout_ms: %d", prof.GetResponseIdleTimeoutMs())
}
if got, ok := profile["persistent"].(bool); !ok || !got {
t.Fatalf("expected persistent=true, got %v", profile["persistent"])
if prof.GetStartupIdleTimeoutMs() != 300 {
t.Fatalf("startup_idle_timeout_ms: %d", prof.GetStartupIdleTimeoutMs())
}
if got, ok := profile["terminal"].(bool); !ok || !got {
t.Fatalf("expected terminal=true, got %v", profile["terminal"])
}
// structpb encodes numbers as float64
if got, ok := profile["response_idle_timeout_ms"].(float64); !ok || got != 1500 {
t.Fatalf("expected response_idle_timeout_ms=1500, got %v", profile["response_idle_timeout_ms"])
}
if got, ok := profile["startup_idle_timeout_ms"].(float64); !ok || got != 300 {
t.Fatalf("expected startup_idle_timeout_ms=300, got %v", profile["startup_idle_timeout_ms"])
}
if got, ok := profile["output_format"].(string); !ok || got != "codex-json" {
t.Fatalf("expected output_format=%q, got %v", "codex-json", profile["output_format"])
if prof.GetOutputFormat() != "codex-json" {
t.Fatalf("output_format: %q", prof.GetOutputFormat())
}
}
func TestBuildNodePayload_CLICompletionMarker(t *testing.T) {
func TestBuildConfigPayload_CLICompletionMarker(t *testing.T) {
rec := &edgenode.NodeRecord{
ID: "node-local",
Alias: "local",
@ -157,33 +150,47 @@ func TestBuildNodePayload_CLICompletionMarker(t *testing.T) {
if err != nil {
t.Fatalf("build config payload: %v", err)
}
var cliSettings map[string]any
for _, adapter := range payload.GetAdapters() {
if adapter.GetType() == "cli" {
cliSettings = adapter.GetSettings().AsMap()
break
}
cli := findCLIAdapter(payload)
if cli == nil {
t.Fatal("expected cli adapter")
}
if cliSettings == nil {
t.Fatal("expected cli adapter settings")
prof := cli.GetCli().GetProfiles()["codex"]
marker := prof.GetCompletionMarker()
if marker == nil {
t.Fatal("expected completion_marker")
}
profiles, ok := cliSettings["profiles"].(map[string]any)
if !ok {
t.Fatalf("expected profiles map, got %T", cliSettings["profiles"])
if marker.GetLine() != "[[DONE]]" {
t.Fatalf("line: %q", marker.GetLine())
}
profile, ok := profiles["codex"].(map[string]any)
if !ok {
t.Fatalf("expected codex profile map, got %T", profiles["codex"])
}
marker, ok := profile["completion_marker"].(map[string]any)
if !ok {
t.Fatalf("expected completion_marker map, got %T", profile["completion_marker"])
}
if got, _ := marker["line"].(string); got != "[[DONE]]" {
t.Fatalf("expected completion_marker.line=%q, got %v", "[[DONE]]", marker["line"])
}
if got, _ := marker["regex"].(string); got != `^\[\[DONE\]\]$` {
t.Fatalf("expected completion_marker.regex=%q, got %v", `^\[\[DONE\]\]$`, marker["regex"])
if marker.GetRegex() != `^\[\[DONE\]\]$` {
t.Fatalf("regex: %q", marker.GetRegex())
}
}
func TestBuildConfigPayload_OllamaVllmOneof(t *testing.T) {
rec := &edgenode.NodeRecord{
Adapters: config.AdaptersConf{
Ollama: config.OllamaConf{Enabled: true, BaseURL: "http://localhost:11434"},
Vllm: config.VllmConf{Enabled: true, Endpoint: "http://localhost:8000"},
},
}
payload, err := buildConfigPayload(rec)
if err != nil {
t.Fatalf("build: %v", err)
}
var ollama, vllm *iop.AdapterConfig
for _, a := range payload.GetAdapters() {
switch a.GetType() {
case "ollama":
ollama = a
case "vllm":
vllm = a
}
}
if ollama == nil || ollama.GetOllama().GetBaseUrl() != "http://localhost:11434" {
t.Fatalf("ollama: %+v", ollama)
}
if vllm == nil || vllm.GetVllm().GetEndpoint() != "http://localhost:8000" {
t.Fatalf("vllm: %+v", vllm)
}
}

View file

@ -27,13 +27,28 @@ func BuildFromPayload(payload *iop.NodeConfigPayload, logger *zap.Logger) (*Regi
case "mock":
// mock is always registered above.
case "ollama":
cfg := ollamaConfFromStruct(ac.GetSettings())
var cfg config.OllamaConf
if m := ac.GetOllama(); m != nil {
cfg = ollamaConfFromProto(m)
} else {
cfg = ollamaConfFromStruct(ac.GetSettings())
}
reg.Register(ollama.New(cfg, logger))
case "vllm":
cfg := vllmConfFromStruct(ac.GetSettings())
var cfg config.VllmConf
if m := ac.GetVllm(); m != nil {
cfg = vllmConfFromProto(m)
} else {
cfg = vllmConfFromStruct(ac.GetSettings())
}
reg.Register(vllm.New(cfg, logger))
case "cli":
cfg := cliConfFromStruct(ac.GetSettings())
var cfg config.CLIConf
if m := ac.GetCli(); m != nil {
cfg = cliConfFromProto(m)
} else {
cfg = cliConfFromStruct(ac.GetSettings())
}
reg.Register(cli.New(cfg, logger))
default:
return nil, fmt.Errorf("adapters: unknown adapter type %q", ac.GetType())
@ -42,12 +57,56 @@ func BuildFromPayload(payload *iop.NodeConfigPayload, logger *zap.Logger) (*Regi
return reg, nil
}
func ollamaConfFromProto(m *iop.OllamaAdapterConfig) config.OllamaConf {
return config.OllamaConf{Enabled: true, BaseURL: m.GetBaseUrl()}
}
func vllmConfFromProto(m *iop.VllmAdapterConfig) config.VllmConf {
return config.VllmConf{Enabled: true, Endpoint: m.GetEndpoint()}
}
func cliConfFromProto(m *iop.CLIAdapterConfig) config.CLIConf {
cfg := config.CLIConf{Enabled: true, Profiles: make(map[string]config.CLIProfileConf)}
for name, p := range m.GetProfiles() {
cfg.Profiles[name] = cliProfileFromProto(p)
}
return cfg
}
func cliProfileFromProto(p *iop.CLIProfileConfig) config.CLIProfileConf {
if p == nil {
return config.CLIProfileConf{}
}
prof := config.CLIProfileConf{
Command: p.GetCommand(),
Args: append([]string(nil), p.GetArgs()...),
Env: append([]string(nil), p.GetEnv()...),
Persistent: p.GetPersistent(),
Terminal: p.GetTerminal(),
ResponseIdleTimeoutMS: int(p.GetResponseIdleTimeoutMs()),
StartupIdleTimeoutMS: int(p.GetStartupIdleTimeoutMs()),
OutputFormat: p.GetOutputFormat(),
Mode: p.GetMode(),
ResumeArgs: append([]string(nil), p.GetResumeArgs()...),
}
if marker := p.GetCompletionMarker(); marker != nil {
prof.CompletionMarker = config.CompletionMarkerConf{
Line: marker.GetLine(),
Regex: marker.GetRegex(),
}
}
return prof
}
// Legacy structpb fallback paths for payloads from older edge versions
// that populate AdapterConfig.settings instead of the typed oneof.
func ollamaConfFromStruct(s *structpb.Struct) config.OllamaConf {
cfg := config.OllamaConf{Enabled: true}
if s == nil {
return config.OllamaConf{}
return cfg
}
m := s.AsMap()
cfg := config.OllamaConf{Enabled: true}
if v, ok := m["base_url"].(string); ok {
cfg.BaseURL = v
}
@ -55,11 +114,11 @@ func ollamaConfFromStruct(s *structpb.Struct) config.OllamaConf {
}
func vllmConfFromStruct(s *structpb.Struct) config.VllmConf {
cfg := config.VllmConf{Enabled: true}
if s == nil {
return config.VllmConf{}
return cfg
}
m := s.AsMap()
cfg := config.VllmConf{Enabled: true}
if v, ok := m["endpoint"].(string); ok {
cfg.Endpoint = v
}
@ -67,68 +126,73 @@ func vllmConfFromStruct(s *structpb.Struct) config.VllmConf {
}
func cliConfFromStruct(s *structpb.Struct) config.CLIConf {
cfg := config.CLIConf{Enabled: true, Profiles: make(map[string]config.CLIProfileConf)}
if s == nil {
return config.CLIConf{}
return cfg
}
m := s.AsMap()
cfg := config.CLIConf{Enabled: true, Profiles: make(map[string]config.CLIProfileConf)}
if profiles, ok := m["profiles"].(map[string]any); ok {
for name, pAny := range profiles {
p, ok := pAny.(map[string]any)
if !ok {
continue
}
prof := config.CLIProfileConf{}
if cmd, ok := p["command"].(string); ok {
prof.Command = cmd
}
if args, ok := p["args"].([]any); ok {
for _, a := range args {
if s, ok := a.(string); ok {
prof.Args = append(prof.Args, s)
}
}
}
if envs, ok := p["env"].([]any); ok {
for _, e := range envs {
if s, ok := e.(string); ok {
prof.Env = append(prof.Env, s)
}
}
}
if v, ok := boolFromAny(p["persistent"]); ok {
prof.Persistent = v
}
if v, ok := boolFromAny(p["terminal"]); ok {
prof.Terminal = v
}
if v, ok := intFromAny(p["response_idle_timeout_ms"]); ok {
prof.ResponseIdleTimeoutMS = v
}
if v, ok := intFromAny(p["startup_idle_timeout_ms"]); ok {
prof.StartupIdleTimeoutMS = v
}
if v, ok := p["output_format"].(string); ok {
prof.OutputFormat = v
}
if marker, ok := p["completion_marker"].(map[string]any); ok {
if v, ok := marker["line"].(string); ok {
prof.CompletionMarker.Line = v
}
if v, ok := marker["regex"].(string); ok {
prof.CompletionMarker.Regex = v
}
}
cfg.Profiles[name] = prof
profiles, ok := m["profiles"].(map[string]any)
if !ok {
return cfg
}
for name, pAny := range profiles {
p, ok := pAny.(map[string]any)
if !ok {
continue
}
prof := config.CLIProfileConf{}
if v, ok := p["command"].(string); ok {
prof.Command = v
}
prof.Args = stringsFromAny(p["args"])
prof.Env = stringsFromAny(p["env"])
if v, ok := boolFromAny(p["persistent"]); ok {
prof.Persistent = v
}
if v, ok := boolFromAny(p["terminal"]); ok {
prof.Terminal = v
}
if v, ok := intFromAny(p["response_idle_timeout_ms"]); ok {
prof.ResponseIdleTimeoutMS = v
}
if v, ok := intFromAny(p["startup_idle_timeout_ms"]); ok {
prof.StartupIdleTimeoutMS = v
}
if v, ok := p["output_format"].(string); ok {
prof.OutputFormat = v
}
if v, ok := p["mode"].(string); ok {
prof.Mode = v
}
prof.ResumeArgs = stringsFromAny(p["resume_args"])
if marker, ok := p["completion_marker"].(map[string]any); ok {
if v, ok := marker["line"].(string); ok {
prof.CompletionMarker.Line = v
}
if v, ok := marker["regex"].(string); ok {
prof.CompletionMarker.Regex = v
}
}
cfg.Profiles[name] = prof
}
return cfg
}
func boolFromAny(v any) (bool, bool) {
if v == nil {
return false, false
func stringsFromAny(v any) []string {
arr, ok := v.([]any)
if !ok {
return nil
}
out := make([]string, 0, len(arr))
for _, a := range arr {
if s, ok := a.(string); ok {
out = append(out, s)
}
}
return out
}
func boolFromAny(v any) (bool, bool) {
if b, ok := v.(bool); ok {
return b, true
}
@ -136,10 +200,6 @@ func boolFromAny(v any) (bool, bool) {
}
func intFromAny(v any) (int, bool) {
if v == nil {
return 0, false
}
// structpb encodes numbers as float64
if f, ok := v.(float64); ok {
return int(f), true
}

View file

@ -4,26 +4,22 @@ import (
"testing"
"google.golang.org/protobuf/types/known/structpb"
iop "iop/proto/gen/iop"
)
func TestCLIConfFromStruct_ProfileRuntimeOptions(t *testing.T) {
st, err := structpb.NewStruct(map[string]any{
"profiles": map[string]any{
"claude": map[string]any{
"command": "claude",
"args": []any{"-p"},
"env": []any{},
"persistent": false,
"terminal": false,
"output_format": "claude-json",
func TestCLIConfFromProto_ProfileRuntimeOptions(t *testing.T) {
m := &iop.CLIAdapterConfig{
Profiles: map[string]*iop.CLIProfileConfig{
"claude": {
Command: "claude",
Args: []string{"-p"},
OutputFormat: "claude-json",
},
},
})
if err != nil {
t.Fatalf("structpb: %v", err)
}
cfg := cliConfFromStruct(st)
cfg := cliConfFromProto(m)
prof, ok := cfg.Profiles["claude"]
if !ok {
@ -40,97 +36,58 @@ func TestCLIConfFromStruct_ProfileRuntimeOptions(t *testing.T) {
}
}
func TestCLIConfFromStruct_ClaudeHeadlessBypassProfile(t *testing.T) {
st, err := structpb.NewStruct(map[string]any{
"profiles": map[string]any{
"claude": map[string]any{
"command": "claude",
"args": []any{"-p", "--dangerously-skip-permissions"},
"env": []any{},
"persistent": false,
"terminal": false,
func TestCLIConfFromProto_ClaudeHeadlessBypassProfile(t *testing.T) {
m := &iop.CLIAdapterConfig{
Profiles: map[string]*iop.CLIProfileConfig{
"claude": {
Command: "claude",
Args: []string{"-p", "--dangerously-skip-permissions"},
},
},
})
if err != nil {
t.Fatalf("structpb: %v", err)
}
cfg := cliConfFromStruct(st)
cfg := cliConfFromProto(m)
prof, ok := cfg.Profiles["claude"]
if !ok {
t.Fatal("expected claude profile")
}
if prof.Command != "claude" {
t.Fatalf("command: got %q", prof.Command)
}
if len(prof.Args) != 2 || prof.Args[0] != "-p" || prof.Args[1] != "--dangerously-skip-permissions" {
t.Fatalf("args: got %#v", prof.Args)
}
if prof.Persistent {
t.Fatal("expected persistent=false")
}
if prof.Terminal {
t.Fatal("expected terminal=false")
if prof.Persistent || prof.Terminal {
t.Fatalf("expected persistent/terminal=false")
}
}
func TestCLIConfFromStruct_GeminiHeadlessYoloProfile(t *testing.T) {
st, err := structpb.NewStruct(map[string]any{
"profiles": map[string]any{
"gemini": map[string]any{
"command": "gemini",
"args": []any{"-p", "--approval-mode", "yolo"},
"env": []any{},
"persistent": false,
"terminal": false,
func TestCLIConfFromProto_GeminiHeadlessYoloProfile(t *testing.T) {
m := &iop.CLIAdapterConfig{
Profiles: map[string]*iop.CLIProfileConfig{
"gemini": {
Command: "gemini",
Args: []string{"-p", "--approval-mode", "yolo"},
},
},
})
if err != nil {
t.Fatalf("structpb: %v", err)
}
cfg := cliConfFromStruct(st)
cfg := cliConfFromProto(m)
prof, ok := cfg.Profiles["gemini"]
if !ok {
t.Fatal("expected gemini profile")
}
if prof.Command != "gemini" {
t.Fatalf("command: got %q", prof.Command)
}
if len(prof.Args) != 3 || prof.Args[0] != "-p" || prof.Args[1] != "--approval-mode" || prof.Args[2] != "yolo" {
t.Fatalf("args: got %#v", prof.Args)
}
if prof.Persistent {
t.Fatal("expected persistent=false")
}
if prof.Terminal {
t.Fatal("expected terminal=false")
}
}
func TestCLIConfFromStruct_ClineConfigProfile(t *testing.T) {
st, err := structpb.NewStruct(map[string]any{
"profiles": map[string]any{
"cline-dgx": map[string]any{
"command": "/config/.npm-global/bin/cline",
"args": []any{"-y", "--json", "--config", "/config/.cline/profiles/ollama-dgx"},
"env": []any{},
"persistent": false,
"terminal": false,
"output_format": "cline-json",
func TestCLIConfFromProto_ClineConfigProfile(t *testing.T) {
m := &iop.CLIAdapterConfig{
Profiles: map[string]*iop.CLIProfileConfig{
"cline-dgx": {
Command: "/config/.npm-global/bin/cline",
Args: []string{"-y", "--json", "--config", "/config/.cline/profiles/ollama-dgx"},
OutputFormat: "cline-json",
},
},
})
if err != nil {
t.Fatalf("structpb: %v", err)
}
cfg := cliConfFromStruct(st)
cfg := cliConfFromProto(m)
prof, ok := cfg.Profiles["cline-dgx"]
if !ok {
t.Fatal("expected cline-dgx profile")
@ -138,7 +95,7 @@ func TestCLIConfFromStruct_ClineConfigProfile(t *testing.T) {
if prof.Command != "/config/.npm-global/bin/cline" {
t.Fatalf("command: got %q", prof.Command)
}
if len(prof.Args) != 4 || prof.Args[0] != "-y" || prof.Args[1] != "--json" || prof.Args[2] != "--config" || prof.Args[3] != "/config/.cline/profiles/ollama-dgx" {
if len(prof.Args) != 4 {
t.Fatalf("args: got %#v", prof.Args)
}
if prof.OutputFormat != "cline-json" {
@ -146,15 +103,106 @@ func TestCLIConfFromStruct_ClineConfigProfile(t *testing.T) {
}
}
func TestCLIConfFromStruct_ProfileCompletionMarker(t *testing.T) {
func TestCLIConfFromProto_ProfileCompletionMarker(t *testing.T) {
m := &iop.CLIAdapterConfig{
Profiles: map[string]*iop.CLIProfileConfig{
"codex": {
Command: "codex",
Persistent: true,
CompletionMarker: &iop.CLICompletionMarker{
Line: "[[DONE]]",
Regex: `^\[\[DONE\]\]$`,
},
},
},
}
cfg := cliConfFromProto(m)
prof := cfg.Profiles["codex"]
if prof.CompletionMarker.Line != "[[DONE]]" {
t.Fatalf("line: got %q", prof.CompletionMarker.Line)
}
if prof.CompletionMarker.Regex != `^\[\[DONE\]\]$` {
t.Fatalf("regex: got %q", prof.CompletionMarker.Regex)
}
}
func TestCLIConfFromProto_PersistentAndIdleTimeouts(t *testing.T) {
m := &iop.CLIAdapterConfig{
Profiles: map[string]*iop.CLIProfileConfig{
"codex": {
Command: "codex",
Persistent: true,
ResponseIdleTimeoutMs: 1500,
StartupIdleTimeoutMs: 300,
},
},
}
cfg := cliConfFromProto(m)
prof := cfg.Profiles["codex"]
if !prof.Persistent {
t.Fatal("persistent")
}
if prof.ResponseIdleTimeoutMS != 1500 {
t.Fatalf("response_idle_timeout_ms: got %d", prof.ResponseIdleTimeoutMS)
}
if prof.StartupIdleTimeoutMS != 300 {
t.Fatalf("startup_idle_timeout_ms: got %d", prof.StartupIdleTimeoutMS)
}
}
func TestCLIConfFromProto_NilSettings(t *testing.T) {
cfg := cliConfFromProto(&iop.CLIAdapterConfig{})
if !cfg.Enabled {
t.Fatal("expected enabled=true")
}
if cfg.Profiles == nil {
t.Fatal("profiles map should be initialized")
}
}
// Legacy structpb fallback regression tests — these exercise the path taken
// when an older edge sends AdapterConfig.settings instead of the typed oneof.
func TestOllamaConfFromStruct_LegacyPayload(t *testing.T) {
st, err := structpb.NewStruct(map[string]any{"base_url": "http://legacy:11434"})
if err != nil {
t.Fatalf("structpb: %v", err)
}
cfg := ollamaConfFromStruct(st)
if !cfg.Enabled {
t.Fatal("expected enabled=true")
}
if cfg.BaseURL != "http://legacy:11434" {
t.Fatalf("base_url: got %q", cfg.BaseURL)
}
}
func TestVllmConfFromStruct_LegacyPayload(t *testing.T) {
st, err := structpb.NewStruct(map[string]any{"endpoint": "http://legacy:8000"})
if err != nil {
t.Fatalf("structpb: %v", err)
}
cfg := vllmConfFromStruct(st)
if !cfg.Enabled {
t.Fatal("expected enabled=true")
}
if cfg.Endpoint != "http://legacy:8000" {
t.Fatalf("endpoint: got %q", cfg.Endpoint)
}
}
func TestCLIConfFromStruct_LegacyPayload(t *testing.T) {
st, err := structpb.NewStruct(map[string]any{
"profiles": map[string]any{
"codex": map[string]any{
"command": "codex",
"args": []any{},
"env": []any{},
"persistent": true,
"terminal": false,
"command": "codex",
"args": []any{"--legacy"},
"env": []any{"K=V"},
"persistent": true,
"terminal": false,
"response_idle_timeout_ms": 1500,
"startup_idle_timeout_ms": 300,
"output_format": "codex-json",
"completion_marker": map[string]any{
"line": "[[DONE]]",
"regex": `^\[\[DONE\]\]$`,
@ -165,24 +213,63 @@ func TestCLIConfFromStruct_ProfileCompletionMarker(t *testing.T) {
if err != nil {
t.Fatalf("structpb: %v", err)
}
cfg := cliConfFromStruct(st)
prof, ok := cfg.Profiles["codex"]
if !ok {
t.Fatal("expected codex profile")
}
if prof.CompletionMarker.Line != "[[DONE]]" {
t.Fatalf("completion_marker.line: got %q", prof.CompletionMarker.Line)
if prof.Command != "codex" {
t.Fatalf("command: got %q", prof.Command)
}
if prof.CompletionMarker.Regex != `^\[\[DONE\]\]$` {
t.Fatalf("completion_marker.regex: got %q", prof.CompletionMarker.Regex)
if len(prof.Args) != 1 || prof.Args[0] != "--legacy" {
t.Fatalf("args: got %#v", prof.Args)
}
if len(prof.Env) != 1 || prof.Env[0] != "K=V" {
t.Fatalf("env: got %#v", prof.Env)
}
if !prof.Persistent {
t.Fatal("persistent")
}
if prof.ResponseIdleTimeoutMS != 1500 {
t.Fatalf("response_idle_timeout_ms: got %d", prof.ResponseIdleTimeoutMS)
}
if prof.StartupIdleTimeoutMS != 300 {
t.Fatalf("startup_idle_timeout_ms: got %d", prof.StartupIdleTimeoutMS)
}
if prof.OutputFormat != "codex-json" {
t.Fatalf("output_format: got %q", prof.OutputFormat)
}
if prof.CompletionMarker.Line != "[[DONE]]" || prof.CompletionMarker.Regex != `^\[\[DONE\]\]$` {
t.Fatalf("completion_marker: got %+v", prof.CompletionMarker)
}
}
func TestCLIConfFromStruct_NilSettings(t *testing.T) {
cfg := cliConfFromStruct(nil)
if cfg.Enabled {
t.Fatal("expected enabled=false for nil settings")
// End-to-end fallback through BuildFromPayload: oneof empty, settings populated.
func TestBuildFromPayload_LegacySettingsFallback(t *testing.T) {
ollamaSt, _ := structpb.NewStruct(map[string]any{"base_url": "http://legacy:11434"})
vllmSt, _ := structpb.NewStruct(map[string]any{"endpoint": "http://legacy:8000"})
cliSt, _ := structpb.NewStruct(map[string]any{
"profiles": map[string]any{
"codex": map[string]any{
"command": "codex",
"args": []any{"--legacy"},
},
},
})
payload := &iop.NodeConfigPayload{
Adapters: []*iop.AdapterConfig{
{Type: "ollama", Enabled: true, Settings: ollamaSt},
{Type: "vllm", Enabled: true, Settings: vllmSt},
{Type: "cli", Enabled: true, Settings: cliSt},
},
}
reg, err := BuildFromPayload(payload, nil)
if err != nil {
t.Fatalf("build: %v", err)
}
for _, name := range []string{"ollama", "vllm", "cli"} {
if _, ok := reg.Get(name); !ok {
t.Fatalf("expected %s adapter registered from legacy settings", name)
}
}
}

View file

@ -4,7 +4,6 @@ import (
"testing"
"go.uber.org/zap"
"google.golang.org/protobuf/types/known/structpb"
"iop/apps/node/internal/adapters"
iop "iop/proto/gen/iop"
@ -24,16 +23,15 @@ func TestBuildFromPayload_MockAlwaysPresent(t *testing.T) {
}
func TestBuildFromPayload_OllamaEnabled(t *testing.T) {
settings, err := structpb.NewStruct(map[string]any{
"base_url": "http://localhost:11434",
})
if err != nil {
t.Fatalf("settings: %v", err)
}
reg, err := adapters.BuildFromPayload(&iop.NodeConfigPayload{
Adapters: []*iop.AdapterConfig{
{Type: "ollama", Enabled: true, Settings: settings},
{
Type: "ollama",
Enabled: true,
Config: &iop.AdapterConfig_Ollama{
Ollama: &iop.OllamaAdapterConfig{BaseUrl: "http://localhost:11434"},
},
},
},
}, zap.NewNop())
if err != nil {
@ -47,6 +45,28 @@ func TestBuildFromPayload_OllamaEnabled(t *testing.T) {
}
}
func TestBuildFromPayload_MultipleAdapters(t *testing.T) {
reg, err := adapters.BuildFromPayload(&iop.NodeConfigPayload{
Adapters: []*iop.AdapterConfig{
{Type: "ollama", Enabled: true, Config: &iop.AdapterConfig_Ollama{Ollama: &iop.OllamaAdapterConfig{BaseUrl: "x"}}},
{Type: "vllm", Enabled: true, Config: &iop.AdapterConfig_Vllm{Vllm: &iop.VllmAdapterConfig{Endpoint: "y"}}},
{Type: "cli", Enabled: true, Config: &iop.AdapterConfig_Cli{Cli: &iop.CLIAdapterConfig{
Profiles: map[string]*iop.CLIProfileConfig{
"codex": {Command: "codex", Persistent: true, ResponseIdleTimeoutMs: 1500, StartupIdleTimeoutMs: 300},
},
}}},
},
}, zap.NewNop())
if err != nil {
t.Fatalf("build from payload: %v", err)
}
for _, name := range []string{"mock", "ollama", "vllm", "cli"} {
if _, ok := reg.Get(name); !ok {
t.Fatalf("expected %s adapter to be registered", name)
}
}
}
func TestBuildFromPayload_UnknownType(t *testing.T) {
_, err := adapters.BuildFromPayload(&iop.NodeConfigPayload{
Adapters: []*iop.AdapterConfig{

View file

@ -1088,10 +1088,16 @@ func (x *NodeConfigPayload) GetRuntime() *NodeRuntimeConfig {
// AdapterConfig describes one adapter to enable on the node.
type AdapterConfig struct {
state protoimpl.MessageState `protogen:"open.v1"`
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` // "mock" | "ollama" | "vllm" | "cli"
Enabled bool `protobuf:"varint,2,opt,name=enabled,proto3" json:"enabled,omitempty"`
Settings *structpb.Struct `protobuf:"bytes,3,opt,name=settings,proto3" json:"settings,omitempty"`
state protoimpl.MessageState `protogen:"open.v1"`
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` // "mock" | "ollama" | "vllm" | "cli"
Enabled bool `protobuf:"varint,2,opt,name=enabled,proto3" json:"enabled,omitempty"`
Settings *structpb.Struct `protobuf:"bytes,3,opt,name=settings,proto3" json:"settings,omitempty"` // legacy/compat path; new adapters use oneof
// Types that are valid to be assigned to Config:
//
// *AdapterConfig_Cli
// *AdapterConfig_Ollama
// *AdapterConfig_Vllm
Config isAdapterConfig_Config `protobuf_oneof:"config"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@ -1147,6 +1153,370 @@ func (x *AdapterConfig) GetSettings() *structpb.Struct {
return nil
}
func (x *AdapterConfig) GetConfig() isAdapterConfig_Config {
if x != nil {
return x.Config
}
return nil
}
func (x *AdapterConfig) GetCli() *CLIAdapterConfig {
if x != nil {
if x, ok := x.Config.(*AdapterConfig_Cli); ok {
return x.Cli
}
}
return nil
}
func (x *AdapterConfig) GetOllama() *OllamaAdapterConfig {
if x != nil {
if x, ok := x.Config.(*AdapterConfig_Ollama); ok {
return x.Ollama
}
}
return nil
}
func (x *AdapterConfig) GetVllm() *VllmAdapterConfig {
if x != nil {
if x, ok := x.Config.(*AdapterConfig_Vllm); ok {
return x.Vllm
}
}
return nil
}
type isAdapterConfig_Config interface {
isAdapterConfig_Config()
}
type AdapterConfig_Cli struct {
Cli *CLIAdapterConfig `protobuf:"bytes,4,opt,name=cli,proto3,oneof"`
}
type AdapterConfig_Ollama struct {
Ollama *OllamaAdapterConfig `protobuf:"bytes,5,opt,name=ollama,proto3,oneof"`
}
type AdapterConfig_Vllm struct {
Vllm *VllmAdapterConfig `protobuf:"bytes,6,opt,name=vllm,proto3,oneof"`
}
func (*AdapterConfig_Cli) isAdapterConfig_Config() {}
func (*AdapterConfig_Ollama) isAdapterConfig_Config() {}
func (*AdapterConfig_Vllm) isAdapterConfig_Config() {}
type CLIAdapterConfig struct {
state protoimpl.MessageState `protogen:"open.v1"`
Profiles map[string]*CLIProfileConfig `protobuf:"bytes,1,rep,name=profiles,proto3" json:"profiles,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CLIAdapterConfig) Reset() {
*x = CLIAdapterConfig{}
mi := &file_proto_iop_runtime_proto_msgTypes[13]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CLIAdapterConfig) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CLIAdapterConfig) ProtoMessage() {}
func (x *CLIAdapterConfig) ProtoReflect() protoreflect.Message {
mi := &file_proto_iop_runtime_proto_msgTypes[13]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CLIAdapterConfig.ProtoReflect.Descriptor instead.
func (*CLIAdapterConfig) Descriptor() ([]byte, []int) {
return file_proto_iop_runtime_proto_rawDescGZIP(), []int{13}
}
func (x *CLIAdapterConfig) GetProfiles() map[string]*CLIProfileConfig {
if x != nil {
return x.Profiles
}
return nil
}
type CLIProfileConfig struct {
state protoimpl.MessageState `protogen:"open.v1"`
Command string `protobuf:"bytes,1,opt,name=command,proto3" json:"command,omitempty"`
Args []string `protobuf:"bytes,2,rep,name=args,proto3" json:"args,omitempty"`
Env []string `protobuf:"bytes,3,rep,name=env,proto3" json:"env,omitempty"`
Persistent bool `protobuf:"varint,4,opt,name=persistent,proto3" json:"persistent,omitempty"`
Terminal bool `protobuf:"varint,5,opt,name=terminal,proto3" json:"terminal,omitempty"`
ResponseIdleTimeoutMs int32 `protobuf:"varint,6,opt,name=response_idle_timeout_ms,json=responseIdleTimeoutMs,proto3" json:"response_idle_timeout_ms,omitempty"`
StartupIdleTimeoutMs int32 `protobuf:"varint,7,opt,name=startup_idle_timeout_ms,json=startupIdleTimeoutMs,proto3" json:"startup_idle_timeout_ms,omitempty"`
OutputFormat string `protobuf:"bytes,8,opt,name=output_format,json=outputFormat,proto3" json:"output_format,omitempty"`
CompletionMarker *CLICompletionMarker `protobuf:"bytes,9,opt,name=completion_marker,json=completionMarker,proto3" json:"completion_marker,omitempty"`
Mode string `protobuf:"bytes,10,opt,name=mode,proto3" json:"mode,omitempty"`
ResumeArgs []string `protobuf:"bytes,11,rep,name=resume_args,json=resumeArgs,proto3" json:"resume_args,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CLIProfileConfig) Reset() {
*x = CLIProfileConfig{}
mi := &file_proto_iop_runtime_proto_msgTypes[14]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CLIProfileConfig) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CLIProfileConfig) ProtoMessage() {}
func (x *CLIProfileConfig) ProtoReflect() protoreflect.Message {
mi := &file_proto_iop_runtime_proto_msgTypes[14]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CLIProfileConfig.ProtoReflect.Descriptor instead.
func (*CLIProfileConfig) Descriptor() ([]byte, []int) {
return file_proto_iop_runtime_proto_rawDescGZIP(), []int{14}
}
func (x *CLIProfileConfig) GetCommand() string {
if x != nil {
return x.Command
}
return ""
}
func (x *CLIProfileConfig) GetArgs() []string {
if x != nil {
return x.Args
}
return nil
}
func (x *CLIProfileConfig) GetEnv() []string {
if x != nil {
return x.Env
}
return nil
}
func (x *CLIProfileConfig) GetPersistent() bool {
if x != nil {
return x.Persistent
}
return false
}
func (x *CLIProfileConfig) GetTerminal() bool {
if x != nil {
return x.Terminal
}
return false
}
func (x *CLIProfileConfig) GetResponseIdleTimeoutMs() int32 {
if x != nil {
return x.ResponseIdleTimeoutMs
}
return 0
}
func (x *CLIProfileConfig) GetStartupIdleTimeoutMs() int32 {
if x != nil {
return x.StartupIdleTimeoutMs
}
return 0
}
func (x *CLIProfileConfig) GetOutputFormat() string {
if x != nil {
return x.OutputFormat
}
return ""
}
func (x *CLIProfileConfig) GetCompletionMarker() *CLICompletionMarker {
if x != nil {
return x.CompletionMarker
}
return nil
}
func (x *CLIProfileConfig) GetMode() string {
if x != nil {
return x.Mode
}
return ""
}
func (x *CLIProfileConfig) GetResumeArgs() []string {
if x != nil {
return x.ResumeArgs
}
return nil
}
type CLICompletionMarker struct {
state protoimpl.MessageState `protogen:"open.v1"`
Line string `protobuf:"bytes,1,opt,name=line,proto3" json:"line,omitempty"`
Regex string `protobuf:"bytes,2,opt,name=regex,proto3" json:"regex,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CLICompletionMarker) Reset() {
*x = CLICompletionMarker{}
mi := &file_proto_iop_runtime_proto_msgTypes[15]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CLICompletionMarker) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CLICompletionMarker) ProtoMessage() {}
func (x *CLICompletionMarker) ProtoReflect() protoreflect.Message {
mi := &file_proto_iop_runtime_proto_msgTypes[15]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CLICompletionMarker.ProtoReflect.Descriptor instead.
func (*CLICompletionMarker) Descriptor() ([]byte, []int) {
return file_proto_iop_runtime_proto_rawDescGZIP(), []int{15}
}
func (x *CLICompletionMarker) GetLine() string {
if x != nil {
return x.Line
}
return ""
}
func (x *CLICompletionMarker) GetRegex() string {
if x != nil {
return x.Regex
}
return ""
}
type OllamaAdapterConfig struct {
state protoimpl.MessageState `protogen:"open.v1"`
BaseUrl string `protobuf:"bytes,1,opt,name=base_url,json=baseUrl,proto3" json:"base_url,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *OllamaAdapterConfig) Reset() {
*x = OllamaAdapterConfig{}
mi := &file_proto_iop_runtime_proto_msgTypes[16]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *OllamaAdapterConfig) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*OllamaAdapterConfig) ProtoMessage() {}
func (x *OllamaAdapterConfig) ProtoReflect() protoreflect.Message {
mi := &file_proto_iop_runtime_proto_msgTypes[16]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use OllamaAdapterConfig.ProtoReflect.Descriptor instead.
func (*OllamaAdapterConfig) Descriptor() ([]byte, []int) {
return file_proto_iop_runtime_proto_rawDescGZIP(), []int{16}
}
func (x *OllamaAdapterConfig) GetBaseUrl() string {
if x != nil {
return x.BaseUrl
}
return ""
}
type VllmAdapterConfig struct {
state protoimpl.MessageState `protogen:"open.v1"`
Endpoint string `protobuf:"bytes,1,opt,name=endpoint,proto3" json:"endpoint,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *VllmAdapterConfig) Reset() {
*x = VllmAdapterConfig{}
mi := &file_proto_iop_runtime_proto_msgTypes[17]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *VllmAdapterConfig) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*VllmAdapterConfig) ProtoMessage() {}
func (x *VllmAdapterConfig) ProtoReflect() protoreflect.Message {
mi := &file_proto_iop_runtime_proto_msgTypes[17]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use VllmAdapterConfig.ProtoReflect.Descriptor instead.
func (*VllmAdapterConfig) Descriptor() ([]byte, []int) {
return file_proto_iop_runtime_proto_rawDescGZIP(), []int{17}
}
func (x *VllmAdapterConfig) GetEndpoint() string {
if x != nil {
return x.Endpoint
}
return ""
}
// NodeRuntimeConfig is the runtime tuning pushed to the node.
type NodeRuntimeConfig struct {
state protoimpl.MessageState `protogen:"open.v1"`
@ -1158,7 +1528,7 @@ type NodeRuntimeConfig struct {
func (x *NodeRuntimeConfig) Reset() {
*x = NodeRuntimeConfig{}
mi := &file_proto_iop_runtime_proto_msgTypes[13]
mi := &file_proto_iop_runtime_proto_msgTypes[18]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1170,7 +1540,7 @@ func (x *NodeRuntimeConfig) String() string {
func (*NodeRuntimeConfig) ProtoMessage() {}
func (x *NodeRuntimeConfig) ProtoReflect() protoreflect.Message {
mi := &file_proto_iop_runtime_proto_msgTypes[13]
mi := &file_proto_iop_runtime_proto_msgTypes[18]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1183,7 +1553,7 @@ func (x *NodeRuntimeConfig) ProtoReflect() protoreflect.Message {
// Deprecated: Use NodeRuntimeConfig.ProtoReflect.Descriptor instead.
func (*NodeRuntimeConfig) Descriptor() ([]byte, []int) {
return file_proto_iop_runtime_proto_rawDescGZIP(), []int{13}
return file_proto_iop_runtime_proto_rawDescGZIP(), []int{18}
}
func (x *NodeRuntimeConfig) GetConcurrency() int32 {
@ -1307,11 +1677,43 @@ const file_proto_iop_runtime_proto_rawDesc = "" +
"\x06config\x18\x05 \x01(\v2\x16.iop.NodeConfigPayloadR\x06config\"u\n" +
"\x11NodeConfigPayload\x12.\n" +
"\badapters\x18\x01 \x03(\v2\x12.iop.AdapterConfigR\badapters\x120\n" +
"\aruntime\x18\x02 \x01(\v2\x16.iop.NodeRuntimeConfigR\aruntime\"r\n" +
"\aruntime\x18\x02 \x01(\v2\x16.iop.NodeRuntimeConfigR\aruntime\"\x89\x02\n" +
"\rAdapterConfig\x12\x12\n" +
"\x04type\x18\x01 \x01(\tR\x04type\x12\x18\n" +
"\aenabled\x18\x02 \x01(\bR\aenabled\x123\n" +
"\bsettings\x18\x03 \x01(\v2\x17.google.protobuf.StructR\bsettings\"\\\n" +
"\bsettings\x18\x03 \x01(\v2\x17.google.protobuf.StructR\bsettings\x12)\n" +
"\x03cli\x18\x04 \x01(\v2\x15.iop.CLIAdapterConfigH\x00R\x03cli\x122\n" +
"\x06ollama\x18\x05 \x01(\v2\x18.iop.OllamaAdapterConfigH\x00R\x06ollama\x12,\n" +
"\x04vllm\x18\x06 \x01(\v2\x16.iop.VllmAdapterConfigH\x00R\x04vllmB\b\n" +
"\x06config\"\xa7\x01\n" +
"\x10CLIAdapterConfig\x12?\n" +
"\bprofiles\x18\x01 \x03(\v2#.iop.CLIAdapterConfig.ProfilesEntryR\bprofiles\x1aR\n" +
"\rProfilesEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12+\n" +
"\x05value\x18\x02 \x01(\v2\x15.iop.CLIProfileConfigR\x05value:\x028\x01\"\x9f\x03\n" +
"\x10CLIProfileConfig\x12\x18\n" +
"\acommand\x18\x01 \x01(\tR\acommand\x12\x12\n" +
"\x04args\x18\x02 \x03(\tR\x04args\x12\x10\n" +
"\x03env\x18\x03 \x03(\tR\x03env\x12\x1e\n" +
"\n" +
"persistent\x18\x04 \x01(\bR\n" +
"persistent\x12\x1a\n" +
"\bterminal\x18\x05 \x01(\bR\bterminal\x127\n" +
"\x18response_idle_timeout_ms\x18\x06 \x01(\x05R\x15responseIdleTimeoutMs\x125\n" +
"\x17startup_idle_timeout_ms\x18\a \x01(\x05R\x14startupIdleTimeoutMs\x12#\n" +
"\routput_format\x18\b \x01(\tR\foutputFormat\x12E\n" +
"\x11completion_marker\x18\t \x01(\v2\x18.iop.CLICompletionMarkerR\x10completionMarker\x12\x12\n" +
"\x04mode\x18\n" +
" \x01(\tR\x04mode\x12\x1f\n" +
"\vresume_args\x18\v \x03(\tR\n" +
"resumeArgs\"?\n" +
"\x13CLICompletionMarker\x12\x12\n" +
"\x04line\x18\x01 \x01(\tR\x04line\x12\x14\n" +
"\x05regex\x18\x02 \x01(\tR\x05regex\"0\n" +
"\x13OllamaAdapterConfig\x12\x19\n" +
"\bbase_url\x18\x01 \x01(\tR\abaseUrl\"/\n" +
"\x11VllmAdapterConfig\x12\x1a\n" +
"\bendpoint\x18\x01 \x01(\tR\bendpoint\"\\\n" +
"\x11NodeRuntimeConfig\x12 \n" +
"\vconcurrency\x18\x01 \x01(\x05R\vconcurrency\x12%\n" +
"\x0eworkspace_root\x18\x02 \x01(\tR\rworkspaceRoot*\x81\x01\n" +
@ -1340,7 +1742,7 @@ func file_proto_iop_runtime_proto_rawDescGZIP() []byte {
}
var file_proto_iop_runtime_proto_enumTypes = make([]protoimpl.EnumInfo, 3)
var file_proto_iop_runtime_proto_msgTypes = make([]protoimpl.MessageInfo, 18)
var file_proto_iop_runtime_proto_msgTypes = make([]protoimpl.MessageInfo, 24)
var file_proto_iop_runtime_proto_goTypes = []any{
(RunSessionMode)(0), // 0: iop.RunSessionMode
(CancelAction)(0), // 1: iop.CancelAction
@ -1358,35 +1760,47 @@ var file_proto_iop_runtime_proto_goTypes = []any{
(*RegisterResponse)(nil), // 13: iop.RegisterResponse
(*NodeConfigPayload)(nil), // 14: iop.NodeConfigPayload
(*AdapterConfig)(nil), // 15: iop.AdapterConfig
(*NodeRuntimeConfig)(nil), // 16: iop.NodeRuntimeConfig
nil, // 17: iop.RunRequest.MetadataEntry
nil, // 18: iop.RunEvent.MetadataEntry
nil, // 19: iop.NodeCommandRequest.MetadataEntry
nil, // 20: iop.AgentUsageStatus.MetadataEntry
(*structpb.Struct)(nil), // 21: google.protobuf.Struct
(*CLIAdapterConfig)(nil), // 16: iop.CLIAdapterConfig
(*CLIProfileConfig)(nil), // 17: iop.CLIProfileConfig
(*CLICompletionMarker)(nil), // 18: iop.CLICompletionMarker
(*OllamaAdapterConfig)(nil), // 19: iop.OllamaAdapterConfig
(*VllmAdapterConfig)(nil), // 20: iop.VllmAdapterConfig
(*NodeRuntimeConfig)(nil), // 21: iop.NodeRuntimeConfig
nil, // 22: iop.RunRequest.MetadataEntry
nil, // 23: iop.RunEvent.MetadataEntry
nil, // 24: iop.NodeCommandRequest.MetadataEntry
nil, // 25: iop.AgentUsageStatus.MetadataEntry
nil, // 26: iop.CLIAdapterConfig.ProfilesEntry
(*structpb.Struct)(nil), // 27: google.protobuf.Struct
}
var file_proto_iop_runtime_proto_depIdxs = []int32{
21, // 0: iop.RunRequest.policy:type_name -> google.protobuf.Struct
21, // 1: iop.RunRequest.input:type_name -> google.protobuf.Struct
17, // 2: iop.RunRequest.metadata:type_name -> iop.RunRequest.MetadataEntry
27, // 0: iop.RunRequest.policy:type_name -> google.protobuf.Struct
27, // 1: iop.RunRequest.input:type_name -> google.protobuf.Struct
22, // 2: iop.RunRequest.metadata:type_name -> iop.RunRequest.MetadataEntry
0, // 3: iop.RunRequest.session_mode:type_name -> iop.RunSessionMode
5, // 4: iop.RunEvent.usage:type_name -> iop.Usage
18, // 5: iop.RunEvent.metadata:type_name -> iop.RunEvent.MetadataEntry
23, // 5: iop.RunEvent.metadata:type_name -> iop.RunEvent.MetadataEntry
1, // 6: iop.CancelRequest.action:type_name -> iop.CancelAction
2, // 7: iop.NodeCommandRequest.type:type_name -> iop.NodeCommandType
19, // 8: iop.NodeCommandRequest.metadata:type_name -> iop.NodeCommandRequest.MetadataEntry
24, // 8: iop.NodeCommandRequest.metadata:type_name -> iop.NodeCommandRequest.MetadataEntry
2, // 9: iop.NodeCommandResponse.type:type_name -> iop.NodeCommandType
10, // 10: iop.NodeCommandResponse.usage_status:type_name -> iop.AgentUsageStatus
20, // 11: iop.AgentUsageStatus.metadata:type_name -> iop.AgentUsageStatus.MetadataEntry
25, // 11: iop.AgentUsageStatus.metadata:type_name -> iop.AgentUsageStatus.MetadataEntry
14, // 12: iop.RegisterResponse.config:type_name -> iop.NodeConfigPayload
15, // 13: iop.NodeConfigPayload.adapters:type_name -> iop.AdapterConfig
16, // 14: iop.NodeConfigPayload.runtime:type_name -> iop.NodeRuntimeConfig
21, // 15: iop.AdapterConfig.settings:type_name -> google.protobuf.Struct
16, // [16:16] is the sub-list for method output_type
16, // [16:16] is the sub-list for method input_type
16, // [16:16] is the sub-list for extension type_name
16, // [16:16] is the sub-list for extension extendee
0, // [0:16] is the sub-list for field type_name
21, // 14: iop.NodeConfigPayload.runtime:type_name -> iop.NodeRuntimeConfig
27, // 15: iop.AdapterConfig.settings:type_name -> google.protobuf.Struct
16, // 16: iop.AdapterConfig.cli:type_name -> iop.CLIAdapterConfig
19, // 17: iop.AdapterConfig.ollama:type_name -> iop.OllamaAdapterConfig
20, // 18: iop.AdapterConfig.vllm:type_name -> iop.VllmAdapterConfig
26, // 19: iop.CLIAdapterConfig.profiles:type_name -> iop.CLIAdapterConfig.ProfilesEntry
18, // 20: iop.CLIProfileConfig.completion_marker:type_name -> iop.CLICompletionMarker
17, // 21: iop.CLIAdapterConfig.ProfilesEntry.value:type_name -> iop.CLIProfileConfig
22, // [22:22] is the sub-list for method output_type
22, // [22:22] is the sub-list for method input_type
22, // [22:22] is the sub-list for extension type_name
22, // [22:22] is the sub-list for extension extendee
0, // [0:22] is the sub-list for field type_name
}
func init() { file_proto_iop_runtime_proto_init() }
@ -1394,13 +1808,18 @@ func file_proto_iop_runtime_proto_init() {
if File_proto_iop_runtime_proto != nil {
return
}
file_proto_iop_runtime_proto_msgTypes[12].OneofWrappers = []any{
(*AdapterConfig_Cli)(nil),
(*AdapterConfig_Ollama)(nil),
(*AdapterConfig_Vllm)(nil),
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_iop_runtime_proto_rawDesc), len(file_proto_iop_runtime_proto_rawDesc)),
NumEnums: 3,
NumMessages: 18,
NumMessages: 24,
NumExtensions: 0,
NumServices: 0,
},

View file

@ -132,7 +132,43 @@ message NodeConfigPayload {
message AdapterConfig {
string type = 1; // "mock" | "ollama" | "vllm" | "cli"
bool enabled = 2;
google.protobuf.Struct settings = 3;
google.protobuf.Struct settings = 3; // legacy/compat path; new adapters use oneof
oneof config {
CLIAdapterConfig cli = 4;
OllamaAdapterConfig ollama = 5;
VllmAdapterConfig vllm = 6;
}
}
message CLIAdapterConfig {
map<string, CLIProfileConfig> profiles = 1;
}
message CLIProfileConfig {
string command = 1;
repeated string args = 2;
repeated string env = 3;
bool persistent = 4;
bool terminal = 5;
int32 response_idle_timeout_ms = 6;
int32 startup_idle_timeout_ms = 7;
string output_format = 8;
CLICompletionMarker completion_marker = 9;
string mode = 10;
repeated string resume_args = 11;
}
message CLICompletionMarker {
string line = 1;
string regex = 2;
}
message OllamaAdapterConfig {
string base_url = 1;
}
message VllmAdapterConfig {
string endpoint = 1;
}
// NodeRuntimeConfig is the runtime tuning pushed to the node.