Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 0 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ on:
push:
branches: [main]
pull_request:
branches: [main]

permissions:
contents: read
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/dependency-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ name: Dependency Review

on:
pull_request:
branches: [main]

permissions:
contents: read
Expand Down
2 changes: 1 addition & 1 deletion buf.gen.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ managed:
inputs:
- git_repo: https://github.com/kontext-security/proto.git
branch: main
ref: eb148cfee8e1d390a3f1e29116d210f2459f4670
ref: ba2b704abfde23ec2c30bfd2b5c540a91a674f41
plugins:
- local: protoc-gen-go
out: gen
Expand Down
265 changes: 228 additions & 37 deletions gen/kontext/agent/v1/agent.pb.go

Large diffs are not rendered by default.

35 changes: 35 additions & 0 deletions gen/kontext/agent/v1/agentv1connect/agent.connect.go

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

9 changes: 9 additions & 0 deletions internal/backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,15 @@ func (c *Client) CreateSession(ctx context.Context, req *agentv1.CreateSessionRe
return resp.Msg, nil
}

// BootstrapCli prepares the shared CLI application for env template sync.
func (c *Client) BootstrapCli(ctx context.Context, req *agentv1.BootstrapCliRequest) (*agentv1.BootstrapCliResponse, error) {
resp, err := c.rpc.BootstrapCli(ctx, connect.NewRequest(req))
if err != nil {
return nil, fmt.Errorf("BootstrapCli: %w", err)
}
return resp.Msg, nil
}

// Heartbeat keeps a session alive.
func (c *Client) Heartbeat(ctx context.Context, sessionID string) error {
_, err := c.rpc.Heartbeat(ctx, connect.NewRequest(&agentv1.HeartbeatRequest{
Expand Down
116 changes: 76 additions & 40 deletions internal/credential/credential.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,85 @@
package credential

import (
"bufio"
"fmt"
"os"
"regexp"
"strings"
)

// placeholder matches {{kontext:<provider>}} or {{kontext:<provider>/<resource>}} patterns.
var placeholder = regexp.MustCompile(`\{\{kontext:([^}]+)\}\}`)
var placeholder = regexp.MustCompile(`^\{\{kontext:([^}]+)\}\}$`)
Comment thread
michiosw marked this conversation as resolved.
Comment thread
michiosw marked this conversation as resolved.

func normalizePlaceholderValue(value string) string {
trimmed := strings.TrimSpace(value)
if idx := inlineCommentIndex(trimmed); idx >= 0 {
trimmed = strings.TrimSpace(trimmed[:idx])
Comment thread
michiosw marked this conversation as resolved.
}

return trimMatchingQuotes(trimmed)
}

// NormalizeEnvValue trims surrounding quotes from dotenv-style values so the
// launched process receives the literal token, not the quote characters.
func NormalizeEnvValue(value string) string {
return normalizePlaceholderValue(value)
}
Comment thread
michiosw marked this conversation as resolved.

func trimMatchingQuotes(value string) string {
if len(value) < 2 {
return value
}

if (value[0] == '"' && value[len(value)-1] == '"') ||
(value[0] == '\'' && value[len(value)-1] == '\'') {
return strings.TrimSpace(value[1 : len(value)-1])
}

return value
}

func inlineCommentIndex(value string) int {
inSingle := false
inDouble := false
escaped := false

for i := 0; i < len(value); i++ {
ch := value[i]
if escaped {
escaped = false
continue
}

switch ch {
case '\\':
if inDouble {
escaped = true
}
case '\'':
if !inDouble {
inSingle = !inSingle
}
case '"':
if !inSingle {
inDouble = !inDouble
}
case '#':
if !inSingle && !inDouble && (i == 0 || isInlineCommentWhitespace(value[i-1])) {
return i
}
}
}

return -1
}

func isInlineCommentWhitespace(ch byte) bool {
switch ch {
case ' ', '\t':
return true
default:
return false
}
}

// Entry represents a single credential placeholder from the env template.
type Entry struct {
Expand All @@ -36,45 +106,11 @@ type Resolved struct {

// ParseTemplate reads an env template file and extracts credential placeholders.
func ParseTemplate(path string) ([]Entry, error) {
f, err := os.Open(path)
doc, err := LoadTemplateFile(path)
if err != nil {
return nil, fmt.Errorf("open env template: %w", err)
return nil, fmt.Errorf("parse template: %w", err)
}
defer f.Close()

var entries []Entry
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}

parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
continue
}

envVar := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])

matches := placeholder.FindStringSubmatch(value)
if matches == nil {
continue
}

providerSpec := matches[1]
provider, resource, _ := strings.Cut(providerSpec, "/")

entries = append(entries, Entry{
EnvVar: envVar,
Provider: provider,
Resource: resource,
Raw: matches[0],
})
}

return entries, scanner.Err()
return doc.Entries, nil
}

// BuildEnv converts resolved credentials into environment variable assignments.
Expand Down
Loading
Loading