write a service in golang that will monitor teleport for new access requests and then evaluate the teleport request with the claude agent and assign it a risk score
  calculation based on if the request has a JIRA ticket (CUSTX project or ITSM) and if it has a description detailing what the request is for


- update-config: Use this skill to configure the Claude Code harness via settings.json. Automated behaviors ("from now on when X", "each time X", "whenever X", "before/after X") require hooks configured in settings.json - the harness executes these, not Claude, so memory/preferences cannot fulfill them. Also use for: permissions ("allow X", "add permission", "move permission to"), env vars ("set X=Y"), hook troubleshooting, or any changes to settings.json/settings.local.json files. Examples: "allow npm commands", "add bq permission to global settings", "move permission to user settings", "set DEBUG=true", "when claude stops show X". For simple settings like theme/model, use Config tool.
- keybindings-help: Use when the user wants to customize keyboard shortcuts, rebind keys, add chord bindings, or modify ~/.claude/keybindings.json. Examples: "rebind ctrl+s", "add a chord shortcut", "change the submit key", "customize keybindings".
- simplify: Review changed code for reuse, quality, and efficiency, then fix any issues found.
- fewer-permission-prompts: Scan your transcripts for common read-only Bash and MCP tool calls, then add a prioritized allowlist to project .claude/settings.json to reduce permission prompts.
- loop: Run a prompt or slash command on a recurring interval (e.g. /loop 5m /foo). Omit the interval to let the model self-pace. - When the user wants to set up a recurring task, poll for status, or run something repeatedly on an interval (e.g. "check the deploy every 5 minutes", "keep running /babysit-prs"). Do NOT invoke for one-off tasks.
- schedule: Create, update, list, or run scheduled remote agents (routines) on a cron schedule or once at a specific time. - When the user wants to schedule a recurring remote agent, set up automated tasks, create a cron job for Claude Code, or manage their scheduled agents/routines. Also use when the user wants a one-time scheduled run ("run this once at 3pm", "remind me to check X tomorrow").
- claude-api: Build, debug, and optimize Claude API / Anthropic SDK apps. Apps built with this skill should include prompt caching. Also handles migrating existing Claude API code between Claude model versions (4.5 → 4.6, 4.6 → 4.7, retired-model replacements).
TRIGGER when: code imports `anthropic`/`@anthropic-ai/sdk`; user asks for the Claude API, Anthropic SDK, or Managed Agents; user adds/modifies/tunes a Claude feature (caching, thinking, compaction, tool use, batch, files, citations, memory) or model (Opus/Sonnet/Haiku) in a file; questions about prompt caching / cache hit rate in an Anthropic SDK project.
SKIP: file imports `openai`/other-provider SDK, filename like `*-openai.py`/`*-generic.py`, provider-neutral code, general programming/ML.
- init: Initialize a new CLAUDE.md file with codebase documentation
- review: Review a pull request
- security-review: Complete a security review of the pending changes on the current branch


Let me explore the current `televibe` directory to understand what's already there before building.


Bash

find /Users/steve.briggs/projects/operations/services/televibe -type f | sort

List all files in televibe directory


List services directory


(Bash completed with no output)


drwxr-xr-x steve.briggs staff 768 B Thu Apr 16 10:43:11 2026 ..
drwxr-xr-x steve.briggs staff 384 B Thu Apr 16 12:16:31 2026 crow
drwxr-xr-x steve.briggs staff 320 B Wed Apr 22 12:57:08 2026 fluxcannon
drwxr-xr-x steve.briggs staff  64 B Wed Apr 22 14:50:02 2026 televibe
drwxr-xr-x steve.briggs staff 160 B Wed Apr 22 14:50:02 2026 .


ls -la /Users/steve.briggs/projects/operations/services/televibe/ && ls -la /Users/steve.briggs/projects/operations/services/crow/ && ls -la /Users/steve.briggs/projects/operations/services/fluxcannon/

Explore existing service directories


drwxr-xr-x steve.briggs staff  64 B Wed Apr 22 14:50:02 2026 .
drwxr-xr-x steve.briggs staff 160 B Wed Apr 22 14:50:02 2026 ..
.rw-r--r-- steve.briggs staff 4.3 KB Wed Mar 25 16:07:38 2026 README.md
drwxr-xr-x steve.briggs staff 128 B  Wed Mar 25 16:07:38 2026 base
drwxr-xr-x steve.briggs staff 160 B  Wed Mar 25 16:07:38 2026 deploy
.rw-r--r-- steve.briggs staff 1.6 KB Wed Mar 25 16:07:38 2026 docker-compose.yaml
.rw-r--r-- steve.briggs staff 2.0 KB Wed Mar 25 16:07:38 2026 go.mod
.rw-r--r-- steve.briggs staff 6.8 KB Wed Mar 25 16:07:38 2026 go.sum
drwxr-xr-x steve.briggs staff 160 B  Wed Mar 25 16:07:38 2026 internal
.rw-r--r-- steve.briggs staff 1.7 KB Wed Mar 25 16:07:38 2026 main.go
drwxr-xr-x steve.briggs staff 192 B  Thu Apr 16 10:43:09 2026 e2e
.rw-r--r-- steve.briggs staff 278 B  Thu Apr 16 10:43:11 2026 Dockerfile
drwxr-xr-x steve.briggs staff 384 B  Thu Apr 16 12:16:31 2026 .
drwxr-xr-x steve.briggs staff 160 B  Wed Apr 22 14:50:02 2026 ..
.rw-r--r-- steve.briggs staff 437 B  Thu Apr 16 10:43:11 2026 Dockerfile
.rw-r--r-- steve.briggs staff 4.7 KB Thu Apr 16 10:43:11 2026 README.md
drwxr-xr-x steve.briggs staff  96 B  Thu Apr 16 10:43:11 2026 base
drwxr-xr-x steve.briggs staff 160 B  Thu Apr 16 10:43:11 2026 deploy
.rw-r--r-- steve.briggs staff 412 B  Thu Apr 16 10:43:11 2026 go.mod
.rw-r--r-- steve.briggs staff 883 B  Thu Apr 16 10:43:11 2026 go.sum
drwxr-xr-x steve.briggs staff 160 B  Thu Apr 16 10:43:11 2026 internal
.rw-r--r-- steve.briggs staff 1.3 KB Thu Apr 16 10:43:11 2026 main.go
drwxr-xr-x steve.briggs staff 320 B  Wed Apr 22 12:57:08 2026 .
drwxr-xr-x steve.briggs staff 160 B  Wed Apr 22 14:50:02 2026 ..


write a service in golang that will monitor teleport for new access requests and then evaluate the teleport request with the claude agent and assign it a risk score   calculation based on if the reque…


Error: File does not exist. Note: your current working directory is /Users/steve.briggs/projects/operations/services/televibe.


1	package main
2
3	import (
4		"context"
5		"errors"
6		"log"
7		"net/http"
8		"os"
9		"os/signal"
10		"syscall"
11		"time"
12
13		awsconfig "github.com/aws/aws-sdk-go-v2/config"
14		"github.com/aws/aws-sdk-go-v2/service/s3"
15		"github.com/immuta/operations/services/crow/internal/config"
16		"github.com/immuta/operations/services/crow/internal/registry"
17		"github.com/immuta/operations/services/crow/internal/storage"
18		"github.com/labstack/echo/v4"
19		"github.com/labstack/echo/v4/middleware"
20	)
21
22	func main() {
23		cfg, err := config.Load()
24		if err != nil {
25			log.Fatalf("configuration error: %v", err)
26		}
27
28		awsCfg, err := awsconfig.LoadDefaultConfig(context.Background(),
29			awsconfig.WithRegion(cfg.AWSRegion),
30		)
31		if err != nil {
32			log.Fatalf("failed to load AWS config: %v", err)
33		}
34
35		var s3Opts []func(*s3.Options)
36		if cfg.S3PathStyle {
37			s3Opts = append(s3Opts, func(o *s3.Options) {
38				o.UsePathStyle = true
39			})
40		}
41
42		var store storage.Client
43		if cfg.S3PresignEndpoint != "" {
44			store = storage.NewS3ClientWithPresignEndpoint(awsCfg, cfg.S3BucketName, cfg.S3PresignEndpoint, s3Opts...)
45		} else {
46			store = storage.NewS3Client(awsCfg, cfg.S3BucketName, s3Opts...)
47		}
48
49		e := echo.New()
50		e.HideBanner = true
51		e.Use(middleware.RequestLogger())
52		e.Use(middleware.Recover())
53
54		h := registry.NewHandler(store, cfg.PresignExpiry, cfg.MaxUploadSize)
55		h.Register(e)
56
57		// Graceful shutdown
58		go func() {
59			if err := e.Start(":" + cfg.Port); err != nil && !errors.Is(err, http.ErrServerClosed) {
60				e.Logger.Fatal("shutting down the server")
61			}
62		}()
63
64		quit := make(chan os.Signal, 1)
65		signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
66		<-quit
67
68		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
69		defer cancel()
70		if err := e.Shutdown(ctx); err != nil {
71			e.Logger.Fatal(err)
72		}
73	}
74

package main

import (
	"context"
	"errors"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

	awsconfig "github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/s3"
	"github.com/immuta/operations/services/crow/internal/config"
	"github.com/immuta/operations/services/crow/internal/registry"
	"github.com/immuta/operations/services/crow/internal/storage"
	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
)

func main() {
	cfg, err := config.Load()
	if err != nil {
		log.Fatalf("configuration error: %v", err)
	}

	awsCfg, err := awsconfig.LoadDefaultConfig(context.Background(),
		awsconfig.WithRegion(cfg.AWSRegion),
	)
	if err != nil {
		log.Fatalf("failed to load AWS config: %v", err)
	}

	var s3Opts []func(*s3.Options)
	if cfg.S3PathStyle {
		s3Opts = append(s3Opts, func(o *s3.Options) {
			o.UsePathStyle = true
		})
	}

	var store storage.Client
	if cfg.S3PresignEndpoint != "" {
		store = storage.NewS3ClientWithPresignEndpoint(awsCfg, cfg.S3BucketName, cfg.S3PresignEndpoint, s3Opts...)
	} else {
		store = storage.NewS3Client(awsCfg, cfg.S3BucketName, s3Opts...)
	}

	e := echo.New()
	e.HideBanner = true
	e.Use(middleware.RequestLogger())
	e.Use(middleware.Recover())

	h := registry.NewHandler(store, cfg.PresignExpiry, cfg.MaxUploadSize)
	h.Register(e)

	// Graceful shutdown
	go func() {
		if err := e.Start(":" + cfg.Port); err != nil && !errors.Is(err, http.ErrServerClosed) {
			e.Logger.Fatal("shutting down the server")
		}
	}()

	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	<-quit

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	if err := e.Shutdown(ctx); err != nil {
		e.Logger.Fatal(err)
	}
}


1	module github.com/immuta/operations/services/crow
2
3	go 1.25.5
4
5	require (
6		github.com/aws/aws-sdk-go-v2 v1.41.1
7		github.com/aws/aws-sdk-go-v2/config v1.32.9
8		github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0
9		github.com/labstack/echo/v4 v4.15.0
10		github.com/stretchr/testify v1.11.1
11	)
12
13	require (
14		github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect
15		github.com/aws/aws-sdk-go-v2/credentials v1.19.9 // indirect
16		github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect
17		github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect
18		github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // indirect
19		github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
20		github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 // indirect
21		github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
22		github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 // indirect
23		github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect
24		github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 // indirect
25		github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect
26		github.com/aws/aws-sdk-go-v2/service/sso v1.30.10 // indirect
27		github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 // indirect
28		github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect
29		github.com/aws/smithy-go v1.24.1 // indirect
30		github.com/davecgh/go-spew v1.1.1 // indirect
31		github.com/labstack/gommon v0.4.2 // indirect
32		github.com/mattn/go-colorable v0.1.14 // indirect
33		github.com/mattn/go-isatty v0.0.20 // indirect
34		github.com/pmezard/go-difflib v1.0.0 // indirect
35		github.com/stretchr/objx v0.5.3 // indirect
36		github.com/valyala/bytebufferpool v1.0.0 // indirect
37		github.com/valyala/fasttemplate v1.2.2 // indirect
38		golang.org/x/crypto v0.48.0 // indirect
39		golang.org/x/net v0.50.0 // indirect
40		golang.org/x/sys v0.41.0 // indirect
41		golang.org/x/text v0.34.0 // indirect
42		golang.org/x/time v0.14.0 // indirect
43		gopkg.in/yaml.v3 v3.0.1 // indirect
44	)
45

module github.com/immuta/operations/services/crow

go 1.25.5

require (
	github.com/aws/aws-sdk-go-v2 v1.41.1
	github.com/aws/aws-sdk-go-v2/config v1.32.9
	github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0
	github.com/labstack/echo/v4 v4.15.0
	github.com/stretchr/testify v1.11.1
)

require (
	github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect
	github.com/aws/aws-sdk-go-v2/credentials v1.19.9 // indirect
	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect
	github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect
	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // indirect
	github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
	github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 // indirect
	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
	github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 // indirect
	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect
	github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 // indirect
	github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect
	github.com/aws/aws-sdk-go-v2/service/sso v1.30.10 // indirect
	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 // indirect
	github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect
	github.com/aws/smithy-go v1.24.1 // indirect
	github.com/davecgh/go-spew v1.1.1 // indirect
	github.com/labstack/gommon v0.4.2 // indirect
	github.com/mattn/go-colorable v0.1.14 // indirect
	github.com/mattn/go-isatty v0.0.20 // indirect
	github.com/pmezard/go-difflib v1.0.0 // indirect
	github.com/stretchr/objx v0.5.3 // indirect
	github.com/valyala/bytebufferpool v1.0.0 // indirect
	github.com/valyala/fasttemplate v1.2.2 // indirect
	golang.org/x/crypto v0.48.0 // indirect
	golang.org/x/net v0.50.0 // indirect
	golang.org/x/sys v0.41.0 // indirect
	golang.org/x/text v0.34.0 // indirect
	golang.org/x/time v0.14.0 // indirect
	gopkg.in/yaml.v3 v3.0.1 // indirect
)


find /Users/steve.briggs/projects/operations/services/crow/internal -type f | sort && find /Users/steve.briggs/projects/operations/services/fluxcannon -type f | sort

Explore crow internal structure and fluxcannon


1	package config
2
3	import (
4		"errors"
5		"fmt"
6		"os"
7		"strconv"
8		"time"
9	)
10
11	const defaultMaxUploadSize = 10 * 1024 * 1024 // 10MB
12
13	type Config struct {
14		Port              string
15		S3BucketName      string
16		AWSRegion         string
17		S3PresignEndpoint string
18		PresignExpiry     time.Duration
19		MaxUploadSize     int64
20		S3PathStyle       bool
21	}
22
23	func Load() (*Config, error) {
24		cfg := &Config{
25			Port:         os.Getenv("PORT"),
26			S3BucketName: os.Getenv("S3_BUCKET_NAME"),
27			AWSRegion:    os.Getenv("AWS_REGION"),
28		}
29
30		cfg.S3PathStyle = os.Getenv("S3_PATH_STYLE") == "true"
31		cfg.S3PresignEndpoint = os.Getenv("S3_PRESIGN_ENDPOINT")
32
33		if v := os.Getenv("MAX_UPLOAD_SIZE"); v != "" {
34			n, err := strconv.ParseInt(v, 10, 64)
35			if err != nil {
36				return nil, fmt.Errorf("MAX_UPLOAD_SIZE is invalid: %w", err)
37			}
38			if n <= 0 {
39				return nil, errors.New("MAX_UPLOAD_SIZE must be positive")
40			}
41			cfg.MaxUploadSize = n
42		} else {
43			cfg.MaxUploadSize = defaultMaxUploadSize
44		}
45
46		if cfg.Port == "" {
47			cfg.Port = "8080"
48		}
49
50		if v := os.Getenv("PRESIGN_EXPIRY"); v != "" {
51			d, err := time.ParseDuration(v)
52			if err != nil {
53				return nil, fmt.Errorf("PRESIGN_EXPIRY is invalid: %w", err)
54			}
55			cfg.PresignExpiry = d
56		} else {
57			cfg.PresignExpiry = 15 * time.Minute
58		}
59
60		if cfg.S3BucketName == "" {
61			return nil, errors.New("S3_BUCKET_NAME is required")
62		}
63
64		if cfg.AWSRegion == "" {
65			return nil, errors.New("AWS_REGION is required")
66		}
67
68		return cfg, nil
69	}
70

package config

import (
	"errors"
	"fmt"
	"os"
	"strconv"
	"time"
)

const defaultMaxUploadSize = 10 * 1024 * 1024 // 10MB

type Config struct {
	Port              string
	S3BucketName      string
	AWSRegion         string
	S3PresignEndpoint string
	PresignExpiry     time.Duration
	MaxUploadSize     int64
	S3PathStyle       bool
}

func Load() (*Config, error) {
	cfg := &Config{
		Port:         os.Getenv("PORT"),
		S3BucketName: os.Getenv("S3_BUCKET_NAME"),
		AWSRegion:    os.Getenv("AWS_REGION"),
	}

	cfg.S3PathStyle = os.Getenv("S3_PATH_STYLE") == "true"
	cfg.S3PresignEndpoint = os.Getenv("S3_PRESIGN_ENDPOINT")

	if v := os.Getenv("MAX_UPLOAD_SIZE"); v != "" {
		n, err := strconv.ParseInt(v, 10, 64)
		if err != nil {
			return nil, fmt.Errorf("MAX_UPLOAD_SIZE is invalid: %w", err)
		}
		if n <= 0 {
			return nil, errors.New("MAX_UPLOAD_SIZE must be positive")
		}
		cfg.MaxUploadSize = n
	} else {
		cfg.MaxUploadSize = defaultMaxUploadSize
	}

	if cfg.Port == "" {
		cfg.Port = "8080"
	}

	if v := os.Getenv("PRESIGN_EXPIRY"); v != "" {
		d, err := time.ParseDuration(v)
		if err != nil {
			return nil, fmt.Errorf("PRESIGN_EXPIRY is invalid: %w", err)
		}
		cfg.PresignExpiry = d
	} else {
		cfg.PresignExpiry = 15 * time.Minute
	}

	if cfg.S3BucketName == "" {
		return nil, errors.New("S3_BUCKET_NAME is required")
	}

	if cfg.AWSRegion == "" {
		return nil, errors.New("AWS_REGION is required")
	}

	return cfg, nil
}


1	FROM golang:1.25-alpine AS builder
2	WORKDIR /app
3	COPY services/crow/go.mod services/crow/go.sum ./
4	RUN go mod download
5	COPY services/crow/ .
6	RUN CGO_ENABLED=0 go build -o crow .
7
8	FROM registry.ic-ops.net/base/rockylinux:9
9	COPY --from=builder /app/crow /crow
10	ENTRYPOINT ["/crow"]
11

FROM golang:1.25-alpine AS builder
WORKDIR /app
COPY services/crow/go.mod services/crow/go.sum ./
RUN go mod download
COPY services/crow/ .
RUN CGO_ENABLED=0 go build -o crow .

FROM registry.ic-ops.net/base/rockylinux:9
COPY --from=builder /app/crow /crow
ENTRYPOINT ["/crow"]


1	module github.com/immuta/operations/services/fluxcannon
2
3	go 1.25.2
4
5	require (
6		github.com/immuta/operations/libs/github v0.0.0-00010101000000-000000000000
7		github.com/stretchr/testify v1.11.1
8	)
9
10	require (
11		github.com/davecgh/go-spew v1.1.1 // indirect
12		github.com/pmezard/go-difflib v1.0.0 // indirect
13		gopkg.in/yaml.v3 v3.0.1 // indirect
14	)
15
16	replace github.com/immuta/operations/libs/github => ../../libs/github
17

module github.com/immuta/operations/services/fluxcannon

go 1.25.2

require (
	github.com/immuta/operations/libs/github v0.0.0-00010101000000-000000000000
	github.com/stretchr/testify v1.11.1
)

require (
	github.com/davecgh/go-spew v1.1.1 // indirect
	github.com/pmezard/go-difflib v1.0.0 // indirect
	gopkg.in/yaml.v3 v3.0.1 // indirect
)

replace github.com/immuta/operations/libs/github => ../../libs/github


Now let me read a couple more reference files to match patterns exactly.


1	package config
2
3	import (
4		"testing"
5		"time"
6
7		"github.com/stretchr/testify/assert"
8		"github.com/stretchr/testify/require"
9	)
10
11	func TestLoad(t *testing.T) {
12		tests := []struct {
13			name                string
14			env                 map[string]string
15			wantErr             string
16			wantPort            string
17			wantBucket          string
18			wantRegion          string
19			wantS3PathStyle     bool
20			wantPresignEndpoint string
21			wantPresignExpiry   time.Duration
22			wantMaxUploadSize   int64
23		}{
24			{
25				name:    "missing S3_BUCKET_NAME",
26				env:     map[string]string{"AWS_REGION": "us-east-1"},
27				wantErr: "S3_BUCKET_NAME is required",
28			},
29			{
30				name:    "missing AWS_REGION",
31				env:     map[string]string{"S3_BUCKET_NAME": "my-bucket"},
32				wantErr: "AWS_REGION is required",
33			},
34			{
35				name:              "defaults",
36				env:               map[string]string{"S3_BUCKET_NAME": "my-bucket", "AWS_REGION": "us-east-1"},
37				wantPort:          "8080",
38				wantBucket:        "my-bucket",
39				wantRegion:        "us-east-1",
40				wantPresignExpiry: 15 * time.Minute,
41				wantMaxUploadSize: 10 * 1024 * 1024,
42			},
43			{
44				name:              "custom port",
45				env:               map[string]string{"S3_BUCKET_NAME": "my-bucket", "AWS_REGION": "eu-west-1", "PORT": "9090"},
46				wantPort:          "9090",
47				wantBucket:        "my-bucket",
48				wantRegion:        "eu-west-1",
49				wantPresignExpiry: 15 * time.Minute,
50				wantMaxUploadSize: 10 * 1024 * 1024,
51			},
52			{
53				name:              "custom presign expiry",
54				env:               map[string]string{"S3_BUCKET_NAME": "my-bucket", "AWS_REGION": "us-east-1", "PRESIGN_EXPIRY": "30m"},
55				wantPort:          "8080",
56				wantBucket:        "my-bucket",
57				wantRegion:        "us-east-1",
58				wantPresignExpiry: 30 * time.Minute,
59				wantMaxUploadSize: 10 * 1024 * 1024,
60			},
61			{
62				name:    "invalid presign expiry",
63				env:     map[string]string{"S3_BUCKET_NAME": "my-bucket", "AWS_REGION": "us-east-1", "PRESIGN_EXPIRY": "notaduration"},
64				wantErr: "PRESIGN_EXPIRY is invalid",
65			},
66			{
67				name:              "S3 path style enabled",
68				env:               map[string]string{"S3_BUCKET_NAME": "my-bucket", "AWS_REGION": "us-east-1", "S3_PATH_STYLE": "true"},
69				wantPort:          "8080",
70				wantBucket:        "my-bucket",
71				wantRegion:        "us-east-1",
72				wantS3PathStyle:   true,
73				wantPresignExpiry: 15 * time.Minute,
74				wantMaxUploadSize: 10 * 1024 * 1024,
75			},
76			{
77				name:              "S3 path style disabled by default",
78				env:               map[string]string{"S3_BUCKET_NAME": "my-bucket", "AWS_REGION": "us-east-1"},
79				wantPort:          "8080",
80				wantBucket:        "my-bucket",
81				wantRegion:        "us-east-1",
82				wantS3PathStyle:   false,
83				wantPresignExpiry: 15 * time.Minute,
84				wantMaxUploadSize: 10 * 1024 * 1024,
85			},
86			{
87				name:                "custom presign endpoint",
88				env:                 map[string]string{"S3_BUCKET_NAME": "my-bucket", "AWS_REGION": "us-east-1", "S3_PRESIGN_ENDPOINT": "http://localhost:9000"},
89				wantPort:            "8080",
90				wantBucket:          "my-bucket",
91				wantRegion:          "us-east-1",
92				wantPresignEndpoint: "http://localhost:9000",
93				wantPresignExpiry:   15 * time.Minute,
94				wantMaxUploadSize:   10 * 1024 * 1024,
95			},
96			{
97				name:              "presign endpoint empty by default",
98				env:               map[string]string{"S3_BUCKET_NAME": "my-bucket", "AWS_REGION": "us-east-1"},
99				wantPort:          "8080",
100				wantBucket:        "my-bucket",
101				wantRegion:        "us-east-1",
102				wantPresignExpiry: 15 * time.Minute,
103				wantMaxUploadSize: 10 * 1024 * 1024,
104			},
105			{
106				name:              "custom max upload size",
107				env:               map[string]string{"S3_BUCKET_NAME": "my-bucket", "AWS_REGION": "us-east-1", "MAX_UPLOAD_SIZE": "5242880"},
108				wantPort:          "8080",
109				wantBucket:        "my-bucket",
110				wantRegion:        "us-east-1",
111				wantMaxUploadSize: 5242880,
112				wantPresignExpiry: 15 * time.Minute,
113			},
114			{
115				name:              "default max upload size",
116				env:               map[string]string{"S3_BUCKET_NAME": "my-bucket", "AWS_REGION": "us-east-1"},
117				wantPort:          "8080",
118				wantBucket:        "my-bucket",
119				wantRegion:        "us-east-1",
120				wantMaxUploadSize: 10 * 1024 * 1024,
121				wantPresignExpiry: 15 * time.Minute,
122			},
123			{
124				name:    "invalid max upload size",
125				env:     map[string]string{"S3_BUCKET_NAME": "my-bucket", "AWS_REGION": "us-east-1", "MAX_UPLOAD_SIZE": "notanumber"},
126				wantErr: "MAX_UPLOAD_SIZE is invalid",
127			},
128			{
129				name:    "zero max upload size",
130				env:     map[string]string{"S3_BUCKET_NAME": "my-bucket", "AWS_REGION": "us-east-1", "MAX_UPLOAD_SIZE": "0"},
131				wantErr: "MAX_UPLOAD_SIZE must be positive",
132			},
133			{
134				name:    "negative max upload size",
135				env:     map[string]string{"S3_BUCKET_NAME": "my-bucket", "AWS_REGION": "us-east-1", "MAX_UPLOAD_SIZE": "-1"},
136				wantErr: "MAX_UPLOAD_SIZE must be positive",
137			},
138		}
139
140		for _, tt := range tests {
141			t.Run(tt.name, func(t *testing.T) {
142				// Clear and set env vars
143				for _, key := range []string{"PORT", "S3_BUCKET_NAME", "AWS_REGION", "PRESIGN_EXPIRY", "S3_PATH_STYLE", "S3_PRESIGN_ENDPOINT", "MAX_UPLOAD_SIZE"} {
144					t.Setenv(key, "")
145				}
146				for k, v := range tt.env {
147					t.Setenv(k, v)
148				}
149
150				cfg, err := Load()
151
152				if tt.wantErr != "" {
153					require.Error(t, err)
154					assert.Contains(t, err.Error(), tt.wantErr)
155					return
156				}
157
158				require.NoError(t, err)
159				assert.Equal(t, tt.wantPort, cfg.Port)
160				assert.Equal(t, tt.wantBucket, cfg.S3BucketName)
161				assert.Equal(t, tt.wantRegion, cfg.AWSRegion)
162				assert.Equal(t, tt.wantS3PathStyle, cfg.S3PathStyle)
163				assert.Equal(t, tt.wantPresignEndpoint, cfg.S3PresignEndpoint)
164				assert.Equal(t, tt.wantPresignExpiry, cfg.PresignExpiry)
165				assert.Equal(t, tt.wantMaxUploadSize, cfg.MaxUploadSize)
166			})
167		}
168	}
169

package config

import (
	"testing"
	"time"

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

func TestLoad(t *testing.T) {
	tests := []struct {
		name                string
		env                 map[string]string
		wantErr             string
		wantPort            string
		wantBucket          string
		wantRegion          string
		wantS3PathStyle     bool
		wantPresignEndpoint string
		wantPresignExpiry   time.Duration
		wantMaxUploadSize   int64
	}{
		{
			name:    "missing S3_BUCKET_NAME",
			env:     map[string]string{"AWS_REGION": "us-east-1"},
			wantErr: "S3_BUCKET_NAME is required",
		},
		{
			name:    "missing AWS_REGION",
			env:     map[string]string{"S3_BUCKET_NAME": "my-bucket"},
			wantErr: "AWS_REGION is required",
		},
		{
			name:              "defaults",
			env:               map[string]string{"S3_BUCKET_NAME": "my-bucket", "AWS_REGION": "us-east-1"},
			wantPort:          "8080",
			wantBucket:        "my-bucket",
			wantRegion:        "us-east-1",
			wantPresignExpiry: 15 * time.Minute,
			wantMaxUploadSize: 10 * 1024 * 1024,
		},
		{
			name:              "custom port",
			env:               map[string]string{"S3_BUCKET_NAME": "my-bucket", "AWS_REGION": "eu-west-1", "PORT": "9090"},
			wantPort:          "9090",
			wantBucket:        "my-bucket",
			wantRegion:        "eu-west-1",
			wantPresignExpiry: 15 * time.Minute,
			wantMaxUploadSize: 10 * 1024 * 1024,
		},
		{
			name:              "custom presign expiry",
			env:               map[string]string{"S3_BUCKET_NAME": "my-bucket", "AWS_REGION": "us-east-1", "PRESIGN_EXPIRY": "30m"},
			wantPort:          "8080",
			wantBucket:        "my-bucket",
			wantRegion:        "us-east-1",
			wantPresignExpiry: 30 * time.Minute,
			wantMaxUploadSize: 10 * 1024 * 1024,
		},
		{
			name:    "invalid presign expiry",
			env:     map[string]string{"S3_BUCKET_NAME": "my-bucket", "AWS_REGION": "us-east-1", "PRESIGN_EXPIRY": "notaduration"},
			wantErr: "PRESIGN_EXPIRY is invalid",
		},
		{
			name:              "S3 path style enabled",
			env:               map[string]string{"S3_BUCKET_NAME": "my-bucket", "AWS_REGION": "us-east-1", "S3_PATH_STYLE": "true"},
			wantPort:          "8080",
			wantBucket:        "my-bucket",
			wantRegion:        "us-east-1",
			wantS3PathStyle:   true,
			wantPresignExpiry: 15 * time.Minute,
			wantMaxUploadSize: 10 * 1024 * 1024,
		},
		{
			name:              "S3 path style disabled by default",
			env:               map[string]string{"S3_BUCKET_NAME": "my-bucket", "AWS_REGION": "us-east-1"},
			wantPort:          "8080",
			wantBucket:        "my-bucket",
			wantRegion:        "us-east-1",
			wantS3PathStyle:   false,
			wantPresignExpiry: 15 * time.Minute,
			wantMaxUploadSize: 10 * 1024 * 1024,
		},
		{
			name:                "custom presign endpoint",
			env:                 map[string]string{"S3_BUCKET_NAME": "my-bucket", "AWS_REGION": "us-east-1", "S3_PRESIGN_ENDPOINT": "http://localhost:9000"},
			wantPort:            "8080",
			wantBucket:          "my-bucket",
			wantRegion:          "us-east-1",
			wantPresignEndpoint: "http://localhost:9000",
			wantPresignExpiry:   15 * time.Minute,
			wantMaxUploadSize:   10 * 1024 * 1024,
		},
		{
			name:              "presign endpoint empty by default",
			env:               map[string]string{"S3_BUCKET_NAME": "my-bucket", "AWS_REGION": "us-east-1"},
			wantPort:          "8080",
			wantBucket:        "my-bucket",
			wantRegion:        "us-east-1",
			wantPresignExpiry: 15 * time.Minute,
			wantMaxUploadSize: 10 * 1024 * 1024,
		},
		{
			name:              "custom max upload size",
			env:               map[string]string{"S3_BUCKET_NAME": "my-bucket", "AWS_REGION": "us-east-1", "MAX_UPLOAD_SIZE": "5242880"},
			wantPort:          "8080",
			wantBucket:        "my-bucket",
			wantRegion:        "us-east-1",
			wantMaxUploadSize: 5242880,
			wantPresignExpiry: 15 * time.Minute,
		},
		{
			name:              "default max upload size",
			env:               map[string]string{"S3_BUCKET_NAME": "my-bucket", "AWS_REGION": "us-east-1"},
			wantPort:          "8080",
			wantBucket:        "my-bucket",
			wantRegion:        "us-east-1",
			wantMaxUploadSize: 10 * 1024 * 1024,
			wantPresignExpiry: 15 * time.Minute,
		},
		{
			name:    "invalid max upload size",
			env:     map[string]string{"S3_BUCKET_NAME": "my-bucket", "AWS_REGION": "us-east-1", "MAX_UPLOAD_SIZE": "notanumber"},
			wantErr: "MAX_UPLOAD_SIZE is invalid",
		},
		{
			name:    "zero max upload size",
			env:     map[string]string{"S3_BUCKET_NAME": "my-bucket", "AWS_REGION": "us-east-1", "MAX_UPLOAD_SIZE": "0"},
			wantErr: "MAX_UPLOAD_SIZE must be positive",
		},
		{
			name:    "negative max upload size",
			env:     map[string]string{"S3_BUCKET_NAME": "my-bucket", "AWS_REGION": "us-east-1", "MAX_UPLOAD_SIZE": "-1"},
			wantErr: "MAX_UPLOAD_SIZE must be positive",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Clear and set env vars
			for _, key := range []string{"PORT", "S3_BUCKET_NAME", "AWS_REGION", "PRESIGN_EXPIRY", "S3_PATH_STYLE", "S3_PRESIGN_ENDPOINT", "MAX_UPLOAD_SIZE"} {
				t.Setenv(key, "")
			}
			for k, v := range tt.env {
				t.Setenv(k, v)
			}

			cfg, err := Load()

			if tt.wantErr != "" {
				require.Error(t, err)
				assert.Contains(t, err.Error(), tt.wantErr)
				return
			}

			require.NoError(t, err)
			assert.Equal(t, tt.wantPort, cfg.Port)
			assert.Equal(t, tt.wantBucket, cfg.S3BucketName)
			assert.Equal(t, tt.wantRegion, cfg.AWSRegion)
			assert.Equal(t, tt.wantS3PathStyle, cfg.S3PathStyle)
			assert.Equal(t, tt.wantPresignEndpoint, cfg.S3PresignEndpoint)
			assert.Equal(t, tt.wantPresignExpiry, cfg.PresignExpiry)
			assert.Equal(t, tt.wantMaxUploadSize, cfg.MaxUploadSize)
		})
	}
}


1	package config
2
3	import (
4		"errors"
5		"os"
6	)
7
8	type Config struct {
9		Port        string
10		GitHubToken string
11		GitHubRepo  string
12	}
13
14	func Load() (*Config, error) {
15		cfg := &Config{
16			Port:        os.Getenv("PORT"),
17			GitHubToken: os.Getenv("GITHUB_TOKEN"),
18			GitHubRepo:  os.Getenv("GITHUB_REPO"),
19		}
20
21		if cfg.Port == "" {
22			cfg.Port = "8080"
23		}
24
25		if cfg.GitHubRepo == "" {
26			cfg.GitHubRepo = "immuta/application-deployment"
27		}
28
29		if cfg.GitHubToken == "" {
30			return nil, errors.New("GITHUB_TOKEN is required")
31		}
32
33		return cfg, nil
34	}
35

package config

import (
	"errors"
	"os"
)

type Config struct {
	Port        string
	GitHubToken string
	GitHubRepo  string
}

func Load() (*Config, error) {
	cfg := &Config{
		Port:        os.Getenv("PORT"),
		GitHubToken: os.Getenv("GITHUB_TOKEN"),
		GitHubRepo:  os.Getenv("GITHUB_REPO"),
	}

	if cfg.Port == "" {
		cfg.Port = "8080"
	}

	if cfg.GitHubRepo == "" {
		cfg.GitHubRepo = "immuta/application-deployment"
	}

	if cfg.GitHubToken == "" {
		return nil, errors.New("GITHUB_TOKEN is required")
	}

	return cfg, nil
}


Good context. Now I'll write the full service with 6 files following the same patterns as `crow` and `fluxcannon`.


Create televibe package directories


Write

module github.com/immuta/operations/services/televibe

go 1.25.5

require (
	github.com/anthropics/anthropic-sdk-go v0.3.0
	github.com/gravitational/teleport/api v17.0.3
	github.com/stretchr/testify v1.11.1
)


File created successfully at: /Users/steve.briggs/projects/operations/services/televibe/go.mod

module github.com/immuta/operations/services/televibe

go 1.25.5

require (
	github.com/anthropics/anthropic-sdk-go v0.3.0
	github.com/gravitational/teleport/api v17.0.3
	github.com/stretchr/testify v1.11.1
)


package config

import (
	"errors"
	"os"
)

type Config struct {
	TeleportAddr         string
	TeleportIdentityFile string
	AnthropicAPIKey      string
	ClaudeModel          string
}

func Load() (*Config, error) {
	cfg := &Config{
		TeleportAddr:         os.Getenv("TELEPORT_ADDR"),
		TeleportIdentityFile: os.Getenv("TELEPORT_IDENTITY_FILE"),
		AnthropicAPIKey:      os.Getenv("ANTHROPIC_API_KEY"),
		ClaudeModel:          os.Getenv("CLAUDE_MODEL"),
	}

	if cfg.ClaudeModel == "" {
		cfg.ClaudeModel = "claude-sonnet-4-6"
	}

	if cfg.TeleportAddr == "" {
		return nil, errors.New("TELEPORT_ADDR is required")
	}
	if cfg.TeleportIdentityFile == "" {
		return nil, errors.New("TELEPORT_IDENTITY_FILE is required")
	}
	if cfg.AnthropicAPIKey == "" {
		return nil, errors.New("ANTHROPIC_API_KEY is required")
	}

	return cfg, nil
}


File created successfully at: /Users/steve.briggs/projects/operations/services/televibe/internal/config/config.go

package config

import (
	"errors"
	"os"
)

type Config struct {
	TeleportAddr         string
	TeleportIdentityFile string
	AnthropicAPIKey      string
	ClaudeModel          string
}

func Load() (*Config, error) {
	cfg := &Config{
		TeleportAddr:         os.Getenv("TELEPORT_ADDR"),
		TeleportIdentityFile: os.Getenv("TELEPORT_IDENTITY_FILE"),
		AnthropicAPIKey:      os.Getenv("ANTHROPIC_API_KEY"),
		ClaudeModel:          os.Getenv("CLAUDE_MODEL"),
	}

	if cfg.ClaudeModel == "" {
		cfg.ClaudeModel = "claude-sonnet-4-6"
	}

	if cfg.TeleportAddr == "" {
		return nil, errors.New("TELEPORT_ADDR is required")
	}
	if cfg.TeleportIdentityFile == "" {
		return nil, errors.New("TELEPORT_IDENTITY_FILE is required")
	}
	if cfg.AnthropicAPIKey == "" {
		return nil, errors.New("ANTHROPIC_API_KEY is required")
	}

	return cfg, nil
}


package config

import (
	"testing"

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

func TestLoad(t *testing.T) {
	allKeys := []string{"TELEPORT_ADDR", "TELEPORT_IDENTITY_FILE", "ANTHROPIC_API_KEY", "CLAUDE_MODEL"}

	tests := []struct {
		name          string
		env           map[string]string
		wantErr       string
		wantModel     string
	}{
		{
			name:    "missing TELEPORT_ADDR",
			env:     map[string]string{"TELEPORT_IDENTITY_FILE": "/id.pem", "ANTHROPIC_API_KEY": "key"},
			wantErr: "TELEPORT_ADDR is required",
		},
		{
			name:    "missing TELEPORT_IDENTITY_FILE",
			env:     map[string]string{"TELEPORT_ADDR": "tp.example.com:443", "ANTHROPIC_API_KEY": "key"},
			wantErr: "TELEPORT_IDENTITY_FILE is required",
		},
		{
			name:    "missing ANTHROPIC_API_KEY",
			env:     map[string]string{"TELEPORT_ADDR": "tp.example.com:443", "TELEPORT_IDENTITY_FILE": "/id.pem"},
			wantErr: "ANTHROPIC_API_KEY is required",
		},
		{
			name: "defaults",
			env: map[string]string{
				"TELEPORT_ADDR":          "tp.example.com:443",
				"TELEPORT_IDENTITY_FILE": "/id.pem",
				"ANTHROPIC_API_KEY":      "sk-ant-key",
			},
			wantModel: "claude-sonnet-4-6",
		},
		{
			name: "custom model",
			env: map[string]string{
				"TELEPORT_ADDR":          "tp.example.com:443",
				"TELEPORT_IDENTITY_FILE": "/id.pem",
				"ANTHROPIC_API_KEY":      "sk-ant-key",
				"CLAUDE_MODEL":           "claude-opus-4-7",
			},
			wantModel: "claude-opus-4-7",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			for _, key := range allKeys {
				t.Setenv(key, "")
			}
			for k, v := range tt.env {
				t.Setenv(k, v)
			}

			cfg, err := Load()

			if tt.wantErr != "" {
				require.Error(t, err)
				assert.Contains(t, err.Error(), tt.wantErr)
				return
			}

			require.NoError(t, err)
			assert.Equal(t, tt.wantModel, cfg.ClaudeModel)
		})
	}
}


File created successfully at: /Users/steve.briggs/projects/operations/services/televibe/internal/config/config_test.go

package config

import (
	"testing"

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

func TestLoad(t *testing.T) {
	allKeys := []string{"TELEPORT_ADDR", "TELEPORT_IDENTITY_FILE", "ANTHROPIC_API_KEY", "CLAUDE_MODEL"}

	tests := []struct {
		name          string
		env           map[string]string
		wantErr       string
		wantModel     string
	}{
		{
			name:    "missing TELEPORT_ADDR",
			env:     map[string]string{"TELEPORT_IDENTITY_FILE": "/id.pem", "ANTHROPIC_API_KEY": "key"},
			wantErr: "TELEPORT_ADDR is required",
		},
		{
			name:    "missing TELEPORT_IDENTITY_FILE",
			env:     map[string]string{"TELEPORT_ADDR": "tp.example.com:443", "ANTHROPIC_API_KEY": "key"},
			wantErr: "TELEPORT_IDENTITY_FILE is required",
		},
		{
			name:    "missing ANTHROPIC_API_KEY",
			env:     map[string]string{"TELEPORT_ADDR": "tp.example.com:443", "TELEPORT_IDENTITY_FILE": "/id.pem"},
			wantErr: "ANTHROPIC_API_KEY is required",
		},
		{
			name: "defaults",
			env: map[string]string{
				"TELEPORT_ADDR":          "tp.example.com:443",
				"TELEPORT_IDENTITY_FILE": "/id.pem",
				"ANTHROPIC_API_KEY":      "sk-ant-key",
			},
			wantModel: "claude-sonnet-4-6",
		},
		{
			name: "custom model",
			env: map[string]string{
				"TELEPORT_ADDR":          "tp.example.com:443",
				"TELEPORT_IDENTITY_FILE": "/id.pem",
				"ANTHROPIC_API_KEY":      "sk-ant-key",
				"CLAUDE_MODEL":           "claude-opus-4-7",
			},
			wantModel: "claude-opus-4-7",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			for _, key := range allKeys {
				t.Setenv(key, "")
			}
			for k, v := range tt.env {
				t.Setenv(k, v)
			}

			cfg, err := Load()

			if tt.wantErr != "" {
				require.Error(t, err)
				assert.Contains(t, err.Error(), tt.wantErr)
				return
			}

			require.NoError(t, err)
			assert.Equal(t, tt.wantModel, cfg.ClaudeModel)
		})
	}
}


package evaluator

import (
	"context"
	"encoding/json"
	"fmt"
	"regexp"
	"strings"

	"github.com/anthropics/anthropic-sdk-go"
	"github.com/anthropics/anthropic-sdk-go/option"
)

// jiraPattern matches CUSTX-NNNN and ITSM-NNNN ticket references (case-insensitive).
var jiraPattern = regexp.MustCompile(`(?i)\b(CUSTX|ITSM)-\d+\b`)

// RiskLevel represents the severity of an access request.
type RiskLevel string

const (
	RiskLow    RiskLevel = "low"
	RiskMedium RiskLevel = "medium"
	RiskHigh   RiskLevel = "high"
)

// Assessment is the structured output of a Claude risk evaluation.
type Assessment struct {
	JIRATicket         string    `json:"jira_ticket"`
	HasJIRATicket      bool      `json:"has_jira_ticket"`
	DescriptionQuality string    `json:"description_quality"`
	RiskScore          int       `json:"risk_score"`
	RiskLevel          RiskLevel `json:"risk_level"`
	Reasoning          string    `json:"reasoning"`
}

// Request holds the access request fields that the evaluator needs.
type Request struct {
	ID     string
	User   string
	Roles  []string
	Reason string
}

// Evaluator calls Claude to assign a risk score to a Teleport access request.
type Evaluator struct {
	client *anthropic.Client
	model  string
}

func New(apiKey, model string) *Evaluator {
	return &Evaluator{
		client: anthropic.NewClient(option.WithAPIKey(apiKey)),
		model:  model,
	}
}

func (e *Evaluator) Evaluate(ctx context.Context, req Request) (*Assessment, error) {
	tickets := jiraPattern.FindAllString(req.Reason, -1)
	prompt := buildPrompt(req.User, req.Roles, req.Reason, tickets)

	msg, err := e.client.Messages.New(ctx, anthropic.MessageNewParams{
		Model:     anthropic.F(anthropic.Model(e.model)),
		MaxTokens: anthropic.F(int64(512)),
		Messages: anthropic.F([]anthropic.MessageParam{
			anthropic.NewUserMessage(anthropic.NewTextBlock(prompt)),
		}),
	})
	if err != nil {
		return nil, fmt.Errorf("claude API: %w", err)
	}

	if len(msg.Content) == 0 {
		return nil, fmt.Errorf("empty response from claude")
	}

	text := strings.TrimSpace(msg.Content[0].Text)
	// Strip markdown code fences that Claude sometimes adds.
	if strings.HasPrefix(text, "```") {
		if i := strings.Index(text[3:], "\n"); i >= 0 {
			text = strings.TrimSuffix(strings.TrimSpace(text[3+i+1:]), "```")
		}
	}

	var a Assessment
	if err := json.Unmarshal([]byte(text), &a); err != nil {
		return nil, fmt.Errorf("parsing response: %w (raw: %s)", err, text)
	}

	return &a, nil
}

func buildPrompt(user string, roles []string, reason string, tickets []string) string {
	ticketInfo := "none"
	if len(tickets) > 0 {
		ticketInfo = strings.Join(tickets, ", ")
	}

	return fmt.Sprintf(`You are a security risk evaluator for Teleport access requests. Respond ONLY with a valid JSON object — no explanation, no markdown.

Access request details:
  User:            %s
  Requested roles: %s
  Reason provided: %q
  JIRA tickets:    %s

Risk scoring rules:
  1–3  (low)    — valid JIRA ticket (CUSTX-NNN or ITSM-NNN) AND a clear description with business justification
  4–6  (medium) — either a valid JIRA ticket OR a clear description, but not both
  7–10 (high)   — no JIRA ticket AND no/vague description (single words or generic phrases like "need access" score 9–10)

Return exactly this JSON shape:
{
  "jira_ticket":          "<ticket ID or null>",
  "has_jira_ticket":      <true|false>,
  "description_quality":  "<none|poor|fair|good|excellent>",
  "risk_score":           <integer 1–10>,
  "risk_level":           "<low|medium|high>",
  "reasoning":            "<one concise sentence>"
}`,
		user,
		strings.Join(roles, ", "),
		reason,
		ticketInfo,
	)
}


File created successfully at: /Users/steve.briggs/projects/operations/services/televibe/internal/evaluator/evaluator.go

package evaluator

import (
	"context"
	"encoding/json"
	"fmt"
	"regexp"
	"strings"

	"github.com/anthropics/anthropic-sdk-go"
	"github.com/anthropics/anthropic-sdk-go/option"
)

// jiraPattern matches CUSTX-NNNN and ITSM-NNNN ticket references (case-insensitive).
var jiraPattern = regexp.MustCompile(`(?i)\b(CUSTX|ITSM)-\d+\b`)

// RiskLevel represents the severity of an access request.
type RiskLevel string

const (
	RiskLow    RiskLevel = "low"
	RiskMedium RiskLevel = "medium"
	RiskHigh   RiskLevel = "high"
)

// Assessment is the structured output of a Claude risk evaluation.
type Assessment struct {
	JIRATicket         string    `json:"jira_ticket"`
	HasJIRATicket      bool      `json:"has_jira_ticket"`
	DescriptionQuality string    `json:"description_quality"`
	RiskScore          int       `json:"risk_score"`
	RiskLevel          RiskLevel `json:"risk_level"`
	Reasoning          string    `json:"reasoning"`
}

// Request holds the access request fields that the evaluator needs.
type Request struct {
	ID     string
	User   string
	Roles  []string
	Reason string
}

// Evaluator calls Claude to assign a risk score to a Teleport access request.
type Evaluator struct {
	client *anthropic.Client
	model  string
}

func New(apiKey, model string) *Evaluator {
	return &Evaluator{
		client: anthropic.NewClient(option.WithAPIKey(apiKey)),
		model:  model,
	}
}

func (e *Evaluator) Evaluate(ctx context.Context, req Request) (*Assessment, error) {
	tickets := jiraPattern.FindAllString(req.Reason, -1)
	prompt := buildPrompt(req.User, req.Roles, req.Reason, tickets)

	msg, err := e.client.Messages.New(ctx, anthropic.MessageNewParams{
		Model:     anthropic.F(anthropic.Model(e.model)),
		MaxTokens: anthropic.F(int64(512)),
		Messages: anthropic.F([]anthropic.MessageParam{
			anthropic.NewUserMessage(anthropic.NewTextBlock(prompt)),
		}),
	})
	if err != nil {
		return nil, fmt.Errorf("claude API: %w", err)
	}

	if len(msg.Content) == 0 {
		return nil, fmt.Errorf("empty response from claude")
	}

	text := strings.TrimSpace(msg.Content[0].Text)
	// Strip markdown code fences that Claude sometimes adds.
	if strings.HasPrefix(text, "```") {
		if i := strings.Index(text[3:], "\n"); i >= 0 {
			text = strings.TrimSuffix(strings.TrimSpace(text[3+i+1:]), "```")
		}
	}

	var a Assessment
	if err := json.Unmarshal([]byte(text), &a); err != nil {
		return nil, fmt.Errorf("parsing response: %w (raw: %s)", err, text)
	}

	return &a, nil
}

func buildPrompt(user string, roles []string, reason string, tickets []string) string {
	ticketInfo := "none"
	if len(tickets) > 0 {
		ticketInfo = strings.Join(tickets, ", ")
	}

	return fmt.Sprintf(`You are a security risk evaluator for Teleport access requests. Respond ONLY with a valid JSON object — no explanation, no markdown.

Access request details:
  User:            %s
  Requested roles: %s
  Reason provided: %q
  JIRA tickets:    %s

Risk scoring rules:
  1–3  (low)    — valid JIRA ticket (CUSTX-NNN or ITSM-NNN) AND a clear description with business justification
  4–6  (medium) — either a valid JIRA ticket OR a clear description, but not both
  7–10 (high)   — no JIRA ticket AND no/vague description (single words or generic phrases like "need access" score 9–10)

Return exactly this JSON shape:
{
  "jira_ticket":          "<ticket ID or null>",
  "has_jira_ticket":      <true|false>,
  "description_quality":  "<none|poor|fair|good|excellent>",
  "risk_score":           <integer 1–10>,
  "risk_level":           "<low|medium|high>",
  "reasoning":            "<one concise sentence>"
}`,
		user,
		strings.Join(roles, ", "),
		reason,
		ticketInfo,
	)
}


package evaluator

import (
	"testing"

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

func TestBuildPrompt_JiraTickets(t *testing.T) {
	tests := []struct {
		name        string
		reason      string
		wantTickets []string
	}{
		{
			name:        "CUSTX ticket",
			reason:      "Working on CUSTX-1234 customer migration",
			wantTickets: []string{"CUSTX-1234"},
		},
		{
			name:        "ITSM ticket",
			reason:      "Resolving ITSM-9999 outage",
			wantTickets: []string{"ITSM-9999"},
		},
		{
			name:        "multiple tickets",
			reason:      "CUSTX-100 and ITSM-200 both require this",
			wantTickets: []string{"CUSTX-100", "ITSM-200"},
		},
		{
			name:        "case insensitive",
			reason:      "custx-555 access needed",
			wantTickets: []string{"custx-555"},
		},
		{
			name:        "no ticket",
			reason:      "I need access to the database",
			wantTickets: nil,
		},
		{
			name:        "unrelated pattern",
			reason:      "PROJ-123 is not a valid project",
			wantTickets: nil,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := jiraPattern.FindAllString(tt.reason, -1)
			assert.Equal(t, tt.wantTickets, got)
		})
	}
}

func TestBuildPrompt_ContainsRequiredFields(t *testing.T) {
	prompt := buildPrompt("alice", []string{"prod-db-readonly", "prod-k8s-viewer"}, "ITSM-42 emergency access needed", []string{"ITSM-42"})

	assert.Contains(t, prompt, "alice")
	assert.Contains(t, prompt, "prod-db-readonly")
	assert.Contains(t, prompt, "prod-k8s-viewer")
	assert.Contains(t, prompt, "ITSM-42")
	assert.Contains(t, prompt, "risk_score")
	assert.Contains(t, prompt, "reasoning")
}


File created successfully at: /Users/steve.briggs/projects/operations/services/televibe/internal/evaluator/evaluator_test.go

package evaluator

import (
	"testing"

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

func TestBuildPrompt_JiraTickets(t *testing.T) {
	tests := []struct {
		name        string
		reason      string
		wantTickets []string
	}{
		{
			name:        "CUSTX ticket",
			reason:      "Working on CUSTX-1234 customer migration",
			wantTickets: []string{"CUSTX-1234"},
		},
		{
			name:        "ITSM ticket",
			reason:      "Resolving ITSM-9999 outage",
			wantTickets: []string{"ITSM-9999"},
		},
		{
			name:        "multiple tickets",
			reason:      "CUSTX-100 and ITSM-200 both require this",
			wantTickets: []string{"CUSTX-100", "ITSM-200"},
		},
		{
			name:        "case insensitive",
			reason:      "custx-555 access needed",
			wantTickets: []string{"custx-555"},
		},
		{
			name:        "no ticket",
			reason:      "I need access to the database",
			wantTickets: nil,
		},
		{
			name:        "unrelated pattern",
			reason:      "PROJ-123 is not a valid project",
			wantTickets: nil,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := jiraPattern.FindAllString(tt.reason, -1)
			assert.Equal(t, tt.wantTickets, got)
		})
	}
}

func TestBuildPrompt_ContainsRequiredFields(t *testing.T) {
	prompt := buildPrompt("alice", []string{"prod-db-readonly", "prod-k8s-viewer"}, "ITSM-42 emergency access needed", []string{"ITSM-42"})

	assert.Contains(t, prompt, "alice")
	assert.Contains(t, prompt, "prod-db-readonly")
	assert.Contains(t, prompt, "prod-k8s-viewer")
	assert.Contains(t, prompt, "ITSM-42")
	assert.Contains(t, prompt, "risk_score")
	assert.Contains(t, prompt, "reasoning")
}


package watcher

import (
	"context"
	"fmt"
	"log/slog"
	"time"

	"github.com/gravitational/teleport/api/client"
	"github.com/gravitational/teleport/api/types"
	"github.com/immuta/operations/services/televibe/internal/evaluator"
)

const reconnectDelay = 5 * time.Second

// Watcher streams pending Teleport access requests onto a channel.
// It reconnects automatically on watcher failure.
type Watcher struct {
	client   *client.Client
	requests chan evaluator.Request
	logger   *slog.Logger
}

func New(ctx context.Context, addr, identityFile string, logger *slog.Logger) (*Watcher, error) {
	clt, err := client.New(ctx, client.Config{
		Addrs: []string{addr},
		Credentials: []client.Credentials{
			client.LoadIdentityFile(identityFile),
		},
	})
	if err != nil {
		return nil, fmt.Errorf("connecting to teleport: %w", err)
	}

	w := &Watcher{
		client:   clt,
		requests: make(chan evaluator.Request, 32),
		logger:   logger,
	}

	go w.watch(ctx)
	return w, nil
}

// Requests returns the channel of incoming pending access requests.
func (w *Watcher) Requests() <-chan evaluator.Request {
	return w.requests
}

// Close shuts down the Teleport client.
func (w *Watcher) Close() {
	w.client.Close()
}

func (w *Watcher) watch(ctx context.Context) {
	defer close(w.requests)

	for {
		if err := w.watchOnce(ctx); err != nil {
			if ctx.Err() != nil {
				return
			}
			w.logger.Error("watcher disconnected, reconnecting", "error", err, "delay", reconnectDelay)
			select {
			case <-time.After(reconnectDelay):
			case <-ctx.Done():
				return
			}
		}
	}
}

func (w *Watcher) watchOnce(ctx context.Context) error {
	watcher, err := w.client.NewWatcher(ctx, types.Watch{
		Kinds: []types.WatchKind{{Kind: types.KindAccessRequest}},
	})
	if err != nil {
		return fmt.Errorf("creating watcher: %w", err)
	}
	defer watcher.Close()

	// The first event must be OpInit confirming the watch is established.
	select {
	case event := <-watcher.Events():
		if event.Type != types.OpInit {
			return fmt.Errorf("expected OpInit, got %v", event.Type)
		}
		w.logger.Info("teleport watcher initialized")
	case <-watcher.Done():
		return fmt.Errorf("watcher closed during init: %w", watcher.Error())
	case <-ctx.Done():
		return ctx.Err()
	}

	for {
		select {
		case event := <-watcher.Events():
			if event.Type != types.OpPut {
				continue
			}
			req, ok := event.Resource.(types.AccessRequest)
			if !ok {
				continue
			}
			// Only forward newly-pending requests; ignore state transitions (approve/deny).
			if req.GetState() != types.RequestState_PENDING {
				continue
			}
			select {
			case w.requests <- toRequest(req):
			case <-ctx.Done():
				return ctx.Err()
			}
		case <-watcher.Done():
			return fmt.Errorf("watcher closed: %w", watcher.Error())
		case <-ctx.Done():
			return ctx.Err()
		}
	}
}

func toRequest(req types.AccessRequest) evaluator.Request {
	return evaluator.Request{
		ID:     req.GetName(),
		User:   req.GetUser(),
		Roles:  req.GetRoles(),
		Reason: req.GetRequestReason(),
	}
}


File created successfully at: /Users/steve.briggs/projects/operations/services/televibe/internal/watcher/watcher.go

package watcher

import (
	"context"
	"fmt"
	"log/slog"
	"time"

	"github.com/gravitational/teleport/api/client"
	"github.com/gravitational/teleport/api/types"
	"github.com/immuta/operations/services/televibe/internal/evaluator"
)

const reconnectDelay = 5 * time.Second

// Watcher streams pending Teleport access requests onto a channel.
// It reconnects automatically on watcher failure.
type Watcher struct {
	client   *client.Client
	requests chan evaluator.Request
	logger   *slog.Logger
}

func New(ctx context.Context, addr, identityFile string, logger *slog.Logger) (*Watcher, error) {
	clt, err := client.New(ctx, client.Config{
		Addrs: []string{addr},
		Credentials: []client.Credentials{
			client.LoadIdentityFile(identityFile),
		},
	})
	if err != nil {
		return nil, fmt.Errorf("connecting to teleport: %w", err)
	}

	w := &Watcher{
		client:   clt,
		requests: make(chan evaluator.Request, 32),
		logger:   logger,
	}

	go w.watch(ctx)
	return w, nil
}

// Requests returns the channel of incoming pending access requests.
func (w *Watcher) Requests() <-chan evaluator.Request {
	return w.requests
}

// Close shuts down the Teleport client.
func (w *Watcher) Close() {
	w.client.Close()
}

func (w *Watcher) watch(ctx context.Context) {
	defer close(w.requests)

	for {
		if err := w.watchOnce(ctx); err != nil {
			if ctx.Err() != nil {
				return
			}
			w.logger.Error("watcher disconnected, reconnecting", "error", err, "delay", reconnectDelay)
			select {
			case <-time.After(reconnectDelay):
			case <-ctx.Done():
				return
			}
		}
	}
}

func (w *Watcher) watchOnce(ctx context.Context) error {
	watcher, err := w.client.NewWatcher(ctx, types.Watch{
		Kinds: []types.WatchKind{{Kind: types.KindAccessRequest}},
	})
	if err != nil {
		return fmt.Errorf("creating watcher: %w", err)
	}
	defer watcher.Close()

	// The first event must be OpInit confirming the watch is established.
	select {
	case event := <-watcher.Events():
		if event.Type != types.OpInit {
			return fmt.Errorf("expected OpInit, got %v", event.Type)
		}
		w.logger.Info("teleport watcher initialized")
	case <-watcher.Done():
		return fmt.Errorf("watcher closed during init: %w", watcher.Error())
	case <-ctx.Done():
		return ctx.Err()
	}

	for {
		select {
		case event := <-watcher.Events():
			if event.Type != types.OpPut {
				continue
			}
			req, ok := event.Resource.(types.AccessRequest)
			if !ok {
				continue
			}
			// Only forward newly-pending requests; ignore state transitions (approve/deny).
			if req.GetState() != types.RequestState_PENDING {
				continue
			}
			select {
			case w.requests <- toRequest(req):
			case <-ctx.Done():
				return ctx.Err()
			}
		case <-watcher.Done():
			return fmt.Errorf("watcher closed: %w", watcher.Error())
		case <-ctx.Done():
			return ctx.Err()
		}
	}
}

func toRequest(req types.AccessRequest) evaluator.Request {
	return evaluator.Request{
		ID:     req.GetName(),
		User:   req.GetUser(),
		Roles:  req.GetRoles(),
		Reason: req.GetRequestReason(),
	}
}


package main

import (
	"context"
	"log/slog"
	"os"
	"os/signal"
	"syscall"

	"github.com/immuta/operations/services/televibe/internal/config"
	"github.com/immuta/operations/services/televibe/internal/evaluator"
	"github.com/immuta/operations/services/televibe/internal/watcher"
)

func main() {
	logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

	cfg, err := config.Load()
	if err != nil {
		logger.Error("configuration error", "error", err)
		os.Exit(1)
	}

	eval := evaluator.New(cfg.AnthropicAPIKey, cfg.ClaudeModel)

	ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
	defer cancel()

	w, err := watcher.New(ctx, cfg.TeleportAddr, cfg.TeleportIdentityFile, logger)
	if err != nil {
		logger.Error("failed to connect to teleport", "error", err)
		os.Exit(1)
	}
	defer w.Close()

	logger.Info("televibe started", "teleport_addr", cfg.TeleportAddr, "model", cfg.ClaudeModel)

	for {
		select {
		case req, ok := <-w.Requests():
			if !ok {
				return
			}
			go evaluate(ctx, eval, req, logger)
		case <-ctx.Done():
			return
		}
	}
}

func evaluate(ctx context.Context, eval *evaluator.Evaluator, req evaluator.Request, logger *slog.Logger) {
	a, err := eval.Evaluate(ctx, req)
	if err != nil {
		logger.Error("evaluation failed", "request_id", req.ID, "user", req.User, "error", err)
		return
	}

	logger.Info("access request evaluated",
		"request_id", req.ID,
		"user", req.User,
		"roles", req.Roles,
		"risk_score", a.RiskScore,
		"risk_level", a.RiskLevel,
		"has_jira_ticket", a.HasJIRATicket,
		"jira_ticket", a.JIRATicket,
		"description_quality", a.DescriptionQuality,
		"reasoning", a.Reasoning,
	)
}


File created successfully at: /Users/steve.briggs/projects/operations/services/televibe/main.go

package main

import (
	"context"
	"log/slog"
	"os"
	"os/signal"
	"syscall"

	"github.com/immuta/operations/services/televibe/internal/config"
	"github.com/immuta/operations/services/televibe/internal/evaluator"
	"github.com/immuta/operations/services/televibe/internal/watcher"
)

func main() {
	logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

	cfg, err := config.Load()
	if err != nil {
		logger.Error("configuration error", "error", err)
		os.Exit(1)
	}

	eval := evaluator.New(cfg.AnthropicAPIKey, cfg.ClaudeModel)

	ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
	defer cancel()

	w, err := watcher.New(ctx, cfg.TeleportAddr, cfg.TeleportIdentityFile, logger)
	if err != nil {
		logger.Error("failed to connect to teleport", "error", err)
		os.Exit(1)
	}
	defer w.Close()

	logger.Info("televibe started", "teleport_addr", cfg.TeleportAddr, "model", cfg.ClaudeModel)

	for {
		select {
		case req, ok := <-w.Requests():
			if !ok {
				return
			}
			go evaluate(ctx, eval, req, logger)
		case <-ctx.Done():
			return
		}
	}
}

func evaluate(ctx context.Context, eval *evaluator.Evaluator, req evaluator.Request, logger *slog.Logger) {
	a, err := eval.Evaluate(ctx, req)
	if err != nil {
		logger.Error("evaluation failed", "request_id", req.ID, "user", req.User, "error", err)
		return
	}

	logger.Info("access request evaluated",
		"request_id", req.ID,
		"user", req.User,
		"roles", req.Roles,
		"risk_score", a.RiskScore,
		"risk_level", a.RiskLevel,
		"has_jira_ticket", a.HasJIRATicket,
		"jira_ticket", a.JIRATicket,
		"description_quality", a.DescriptionQuality,
		"reasoning", a.Reasoning,
	)
}


FROM golang:1.25-alpine AS builder
WORKDIR /app
COPY services/televibe/go.mod services/televibe/go.sum ./
RUN go mod download
COPY services/televibe/ .
RUN CGO_ENABLED=0 go build -o televibe .

FROM registry.ic-ops.net/base/rockylinux:9
COPY --from=builder /app/televibe /televibe
ENTRYPOINT ["/televibe"]


File created successfully at: /Users/steve.briggs/projects/operations/services/televibe/Dockerfile

FROM golang:1.25-alpine AS builder
WORKDIR /app
COPY services/televibe/go.mod services/televibe/go.sum ./
RUN go mod download
COPY services/televibe/ .
RUN CGO_ENABLED=0 go build -o televibe .

FROM registry.ic-ops.net/base/rockylinux:9
COPY --from=builder /app/televibe /televibe
ENTRYPOINT ["/televibe"]


Now run `go mod tidy` to resolve the dependency graph and generate the lockfile:


Bash

GONOSUMCHECK='github.com/immuta/*' GOFLAGS='-mod=mod' go mod tidy 2>&1

Resolve dependencies and generate go.sum


Error: Exit code 1
go: errors parsing go.mod:
go.mod:7:2: require github.com/gravitational/teleport/api: version "v17.0.3" invalid: should be v0 or v1, not v17


GONOSUMCHECK='github.com/immuta/*' GONOSUMDB='github.com/immuta/*' go list -m -versions github.com/gravitational/teleport/api 2>&1 | tr ' ' '\n' | tail -20

Check available Teleport API versions


go:
errors
parsing
go.mod:
go.mod:7:2:
require
github.com/gravitational/teleport/api:
version
"v17.0.3"
invalid:
should
be
v0
or
v1,
not
v17


The file /Users/steve.briggs/projects/operations/services/televibe/go.mod has been updated successfully.

module github.com/immuta/operations/services/televibe

go 1.25.5

require (
	github.com/anthropics/anthropic-sdk-go v0.3.0
	github.com/gravitational/teleport/api v17.0.3
	github.com/stretchr/testify v1.11.1
)

-  github.com/gravitational/teleport/api v17.0.3

+  github.com/gravitational/teleport/api v1.0.0-alpha


GONOSUMCHECK='github.com/immuta/*' go list -m -versions github.com/gravitational/teleport/api 2>&1 | tr ' ' '\n' | grep -v '^$' | tail -20

List available Teleport API versions


go:
github.com/gravitational/teleport/api@v1.0.0-alpha:
invalid
version:
unknown
revision
api/v1.0.0-alpha


GONOSUMCHECK='github.com/immuta/*' GOPROXY='https://proxy.golang.org,direct' go list -m -versions github.com/gravitational/teleport/api 2>&1 | tr ' ' '\n' | tail -30

List Teleport API versions via Go proxy


GONOSUMCHECK='github.com/immuta/*' GOPROXY='https://proxy.golang.org,direct' go list -m -versions github.com/gravitational/teleport/api@latest 2>&1

Find latest Teleport API version


GOPROXY='https://proxy.golang.org,direct' go get github.com/gravitational/teleport/api@latest 2>&1

Resolve latest Teleport API version


Error: Exit code 1
go: github.com/anthropics/anthropic-sdk-go@v0.3.0: reading github.com/anthropics/anthropic-sdk-go/go.mod at revision v0.3.0: unknown revision v0.3.0


GOPROXY='https://proxy.golang.org,direct' go list -m -versions github.com/anthropics/anthropic-sdk-go 2>&1 | tr ' ' '\n' | tail -20

List available Anthropic SDK versions


go:
github.com/anthropics/anthropic-sdk-go@v0.3.0:
invalid
version:
unknown
revision
v0.3.0


GOPROXY='https://proxy.golang.org,direct' go list -m -json github.com/anthropics/anthropic-sdk-go@latest 2>&1

Find latest Anthropic SDK version


Question: latest

Question: latest


GOPROXY='https://proxy.golang.org,direct' go list -m -json github.com/gravitational/teleport/api@latest 2>&1

Find latest Teleport API version


Question: latest

Question: latest


Good — now I have the real versions. Let me also check the Anthropic SDK's actual API surface at v1.37.0 before I finalize the evaluator code.


Read Anthropic SDK go.mod


ls /Users/steve.briggs/go/pkg/mod/github.com/anthropics/anthropic-sdk-go@v1.37.0/ 2>/dev/null || echo "not extracted"

Check if Anthropic SDK is extracted in module cache


module github.com/anthropics/anthropic-sdk-go

go 1.23.0

toolchain go1.25.8

require (
	github.com/aws/aws-sdk-go-v2 v1.30.3
	github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3
	github.com/aws/aws-sdk-go-v2/config v1.27.27
	github.com/aws/aws-sdk-go-v2/credentials v1.17.27
	github.com/aws/smithy-go v1.20.3
	github.com/dnaeon/go-vcr v1.2.0
	github.com/invopop/jsonschema v0.13.0
	github.com/stretchr/testify v1.8.4
	github.com/tidwall/gjson v1.18.0
	github.com/tidwall/sjson v1.2.5
	golang.org/x/oauth2 v0.30.0
	golang.org/x/sync v0.16.0
	google.golang.org/api v0.189.0
)

require (
	cloud.google.com/go/auth v0.7.2 // indirect
	cloud.google.com/go/auth/oauth2adapt v0.2.3 // indirect
	cloud.google.com/go/compute/metadata v0.5.0 // indirect
	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 // indirect
	github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 // indirect
	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 // indirect
	github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect
	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect
	github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect
	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect
	github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect
	github.com/bahlo/generic-list-go v0.2.0 // indirect
	github.com/buger/jsonparser v1.1.2 // indirect
	github.com/davecgh/go-spew v1.1.1 // indirect
	github.com/felixge/httpsnoop v1.0.4 // indirect
	github.com/go-logr/logr v1.4.2 // indirect
	github.com/go-logr/stdr v1.2.2 // indirect
	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
	github.com/golang/protobuf v1.5.4 // indirect
	github.com/google/s2a-go v0.1.7 // indirect
	github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
	github.com/mailru/easyjson v0.7.7 // indirect
	github.com/pmezard/go-difflib v1.0.0 // indirect
	github.com/tidwall/match v1.1.1 // indirect
	github.com/tidwall/pretty v1.2.1 // indirect
	github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
	go.opencensus.io v0.24.0 // indirect
	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect
	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
	go.opentelemetry.io/otel v1.24.0 // indirect
	go.opentelemetry.io/otel/metric v1.24.0 // indirect
	go.opentelemetry.io/otel/trace v1.24.0 // indirect
	golang.org/x/crypto v0.40.0 // indirect
	golang.org/x/net v0.41.0 // indirect
	golang.org/x/sys v0.34.0 // indirect
	golang.org/x/text v0.27.0 // indirect
	golang.org/x/time v0.5.0 // indirect
	google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade // indirect
	google.golang.org/grpc v1.64.1 // indirect
	google.golang.org/protobuf v1.34.2 // indirect
	gopkg.in/yaml.v2 v2.2.8 // indirect
	gopkg.in/yaml.v3 v3.0.1 // indirect
)


not extracted


GOPROXY='https://proxy.golang.org,direct' go get github.com/anthropics/anthropic-sdk-go@v1.37.0 2>&1 | head -5

Download Anthropic SDK to extract it


go: github.com/anthropics/anthropic-sdk-go@v0.3.0: reading github.com/anthropics/anthropic-sdk-go/go.mod at revision v0.3.0: unknown revision v0.3.0


Edit

	github.com/anthropics/anthropic-sdk-go v0.3.0
	github.com/gravitational/teleport/api v1.0.0-alpha

	github.com/anthropics/anthropic-sdk-go v1.37.0
	github.com/gravitational/teleport/api v0.0.0-20260422182531-ef25e8e6ba4d


	github.com/anthropics/anthropic-sdk-go v1.37.0
	github.com/gravitational/teleport/api v0.0.0-20260422182531-ef25e8e6ba4d

module github.com/immuta/operations/services/televibe

go 1.25.5

require (
	github.com/anthropics/anthropic-sdk-go v0.3.0
	github.com/gravitational/teleport/api v1.0.0-alpha
	github.com/stretchr/testify v1.11.1
)

-  github.com/anthropics/anthropic-sdk-go v0.3.0

-  github.com/gravitational/teleport/api v1.0.0-alpha

+  github.com/anthropics/anthropic-sdk-go v1.37.0

+  github.com/gravitational/teleport/api v0.0.0-20260422182531-ef25e8e6ba4d


Bash

GOPROXY='https://proxy.golang.org,athens.infrastructure.immuta.io,direct' GONOSUMCHECK='github.com/immuta/*' go mod tidy 2>&1

Run go mod tidy with correct dependency versions


go: downloading github.com/gravitational/teleport/api v0.0.0-20260422182531-ef25e8e6ba4d
go: downloading github.com/anthropics/anthropic-sdk-go v1.37.0
go: github.com/gravitational/teleport/api@v0.0.0-20260422182531-ef25e8e6ba4d requires go >= 1.25.9; switching to go1.25.9
go: downloading go1.25.9 (darwin/arm64)
go: downloading github.com/tidwall/gjson v1.18.0
go: downloading github.com/dnaeon/go-vcr v1.2.0
go: downloading github.com/gobwas/ws v1.4.0
go: downloading github.com/gravitational/trace v1.5.3
go: downloading github.com/jonboulle/clockwork v0.5.0
go: downloading go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0
go: downloading go.opentelemetry.io/otel/trace v1.43.0
go: downloading google.golang.org/grpc v1.80.0
go: downloading go.opentelemetry.io/otel v1.43.0
go: downloading github.com/charlievieth/strcase v0.0.5
go: downloading github.com/russellhaering/gosaml2 v0.11.0
go: downloading go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0
go: downloading go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0
go: downloading google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d
go: downloading go.opentelemetry.io/proto/otlp v1.10.0
go: downloading go.opentelemetry.io/otel/metric v1.43.0
go: downloading go.opentelemetry.io/otel/sdk v1.43.0
go: downloading github.com/gobwas/httphead v0.1.0
go: downloading github.com/gobwas/pool v0.2.1
go: downloading go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0
go: downloading github.com/go-piv/piv-go/v2 v2.5.0
go: downloading golang.org/x/term v0.41.0
go: downloading github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0
go: downloading github.com/russellhaering/goxmldsig v1.6.0
go: downloading github.com/mattermost/xml-roundtrip-validator v0.1.0
go: downloading github.com/beevik/etree v1.6.0
go: downloading google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9
go: downloading go.opentelemetry.io/otel/sdk/metric v1.43.0
go: downloading gonum.org/v1/gonum v0.17.0


1	module github.com/immuta/operations/services/televibe
2
3	go 1.25.9
4
5	require (
6		github.com/anthropics/anthropic-sdk-go v1.37.0
7		github.com/gravitational/teleport/api v0.0.0-20260422182531-ef25e8e6ba4d
8		github.com/stretchr/testify v1.11.1
9	)
10
11	require (
12		github.com/beevik/etree v1.6.0 // indirect
13		github.com/cenkalti/backoff/v5 v5.0.3 // indirect
14		github.com/cespare/xxhash/v2 v2.3.0 // indirect
15		github.com/charlievieth/strcase v0.0.5 // indirect
16		github.com/coreos/go-semver v0.3.1 // indirect
17		github.com/davecgh/go-spew v1.1.1 // indirect
18		github.com/felixge/httpsnoop v1.0.4 // indirect
19		github.com/go-logr/logr v1.4.3 // indirect
20		github.com/go-logr/stdr v1.2.2 // indirect
21		github.com/go-piv/piv-go/v2 v2.5.0 // indirect
22		github.com/gobwas/httphead v0.1.0 // indirect
23		github.com/gobwas/pool v0.2.1 // indirect
24		github.com/gobwas/ws v1.4.0 // indirect
25		github.com/gogo/protobuf v1.3.2 // indirect
26		github.com/google/uuid v1.6.0 // indirect
27		github.com/gravitational/trace v1.5.3 // indirect
28		github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
29		github.com/jonboulle/clockwork v0.5.0 // indirect
30		github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect
31		github.com/pmezard/go-difflib v1.0.0 // indirect
32		github.com/russellhaering/gosaml2 v0.11.0 // indirect
33		github.com/russellhaering/goxmldsig v1.6.0 // indirect
34		github.com/tidwall/gjson v1.18.0 // indirect
35		github.com/tidwall/match v1.1.1 // indirect
36		github.com/tidwall/pretty v1.2.1 // indirect
37		github.com/tidwall/sjson v1.2.5 // indirect
38		go.opentelemetry.io/auto/sdk v1.2.1 // indirect
39		go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 // indirect
40		go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect
41		go.opentelemetry.io/otel v1.43.0 // indirect
42		go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect
43		go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 // indirect
44		go.opentelemetry.io/otel/metric v1.43.0 // indirect
45		go.opentelemetry.io/otel/sdk v1.43.0 // indirect
46		go.opentelemetry.io/otel/trace v1.43.0 // indirect
47		go.opentelemetry.io/proto/otlp v1.10.0 // indirect
48		golang.org/x/crypto v0.49.0 // indirect
49		golang.org/x/net v0.52.0 // indirect
50		golang.org/x/sync v0.20.0 // indirect
51		golang.org/x/sys v0.42.0 // indirect
52		golang.org/x/term v0.41.0 // indirect
53		golang.org/x/text v0.35.0 // indirect
54		google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
55		google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d // indirect
56		google.golang.org/grpc v1.80.0 // indirect
57		google.golang.org/protobuf v1.36.11 // indirect
58		gopkg.in/yaml.v2 v2.4.0 // indirect
59		gopkg.in/yaml.v3 v3.0.1 // indirect
60	)


Dependencies resolved. Now let me verify the SDK API is correct for v1.37.0 before checking if the build compiles.


ls /Users/steve.briggs/go/pkg/mod/github.com/anthropics/anthropic-sdk-go@v1.37.0/ 2>/dev/null | head -20

Check if SDK is extracted


ls /Users/steve.briggs/go/pkg/mod/github.com/gravitational/teleport/api@v0.0.0-20260422182531-ef25e8e6ba4d/client/ 2>/dev/null | head -20

Check Teleport API client package


drwxr-xr-x steve.briggs staff  128 B  Wed Apr 22 15:20:36 2026 ..
dr-xr-xr-x steve.briggs staff   96 B  Wed Apr 22 15:20:36 2026 .devcontainer
dr-xr-xr-x steve.briggs staff  128 B  Wed Apr 22 15:20:36 2026 .github
.r--r--r-- steve.briggs staff   59 B  Wed Apr 22 15:20:36 2026 .gitignore
.r--r--r-- steve.briggs staff   20 B  Wed Apr 22 15:20:36 2026 .release-please-manifest.json
.r--r--r-- steve.briggs staff  291 B  Wed Apr 22 15:20:36 2026 .stats.yml
.r--r--r-- steve.briggs staff   10 B  Wed Apr 22 15:20:36 2026 Brewfile
.r--r--r-- steve.briggs staff   50 KB Wed Apr 22 15:20:36 2026 CHANGELOG.md
.r--r--r-- steve.briggs staff  1.6 KB Wed Apr 22 15:20:36 2026 CONTRIBUTING.md
.r--r--r-- steve.briggs staff  1.0 KB Wed Apr 22 15:20:36 2026 LICENSE
.r--r--r-- steve.briggs staff  1.7 KB Wed Apr 22 15:20:36 2026 README.md
.r--r--r-- steve.briggs staff  1.2 KB Wed Apr 22 15:20:36 2026 SECURITY.md
.r--r--r-- steve.briggs staff  3.9 KB Wed Apr 22 15:20:36 2026 aliases.go
.r--r--r-- steve.briggs staff  190 KB Wed Apr 22 15:20:36 2026 api.md
dr-xr-xr-x steve.briggs staff  160 B  Wed Apr 22 15:20:36 2026 aws
dr-xr-xr-x steve.briggs staff  224 B  Wed Apr 22 15:20:36 2026 bedrock
.r--r--r-- steve.briggs staff   14 KB Wed Apr 22 15:20:36 2026 beta.go
.r--r--r-- steve.briggs staff   84 KB Wed Apr 22 15:20:36 2026 betaagent.go
.r--r--r-- steve.briggs staff  8.3 KB Wed Apr 22 15:20:36 2026 betaagent_test.go
.r--r--r-- steve.briggs staff  3.2 KB Wed Apr 22 15:20:36 2026 betaagentversion.go


.r--r--r-- steve.briggs staff 714 B  Wed Apr 22 15:20:36 2026 README.md
dr-xr-xr-x steve.briggs staff  96 B  Wed Apr 22 15:20:36 2026 accesslist
dr-xr-xr-x steve.briggs staff  96 B  Wed Apr 22 15:20:36 2026 accessmonitoringrules
.r--r--r-- steve.briggs staff 6.1 KB Wed Apr 22 15:20:36 2026 alpn.go
.r--r--r-- steve.briggs staff 9.3 KB Wed Apr 22 15:20:36 2026 alpn_conn_upgrade.go
.r--r--r-- steve.briggs staff 9.9 KB Wed Apr 22 15:20:36 2026 alpn_conn_upgrade_test.go
.r--r--r-- steve.briggs staff 2.6 KB Wed Apr 22 15:20:36 2026 alpn_test.go
.r--r--r-- steve.briggs staff 5.3 KB Wed Apr 22 15:20:36 2026 alpn_websocket.go
.r--r--r-- steve.briggs staff 2.5 KB Wed Apr 22 15:20:36 2026 apln_websocket_test.go
.r--r--r-- steve.briggs staff 4.9 KB Wed Apr 22 15:20:36 2026 auditstreamer.go
.r--r--r-- steve.briggs staff 232 KB Wed Apr 22 15:20:36 2026 client.go
.r--r--r-- steve.briggs staff 6.2 KB Wed Apr 22 15:20:36 2026 client_subca_test.go
.r--r--r-- steve.briggs staff  35 KB Wed Apr 22 15:20:36 2026 client_test.go
.r--r--r-- steve.briggs staff  14 KB Wed Apr 22 15:20:36 2026 contextdialer.go
.r--r--r-- steve.briggs staff  21 KB Wed Apr 22 15:20:36 2026 credentials.go
.r--r--r-- steve.briggs staff  22 KB Wed Apr 22 15:20:36 2026 credentials_test.go
dr-xr-xr-x steve.briggs staff  96 B  Wed Apr 22 15:20:36 2026 crownjewel
dr-xr-xr-x steve.briggs staff  96 B  Wed Apr 22 15:20:36 2026 databaseobject
dr-xr-xr-x steve.briggs staff  96 B  Wed Apr 22 15:20:36 2026 discoveryconfig
.r--r--r-- steve.briggs staff 774 B  Wed Apr 22 15:20:36 2026 doc.go


grep -n 'func NewClient\|type Client\|func New(' /Users/steve.briggs/go/pkg/mod/github.com/anthropics/anthropic-sdk-go@v1.37.0/client.go 2>/dev/null | head -20

Check Anthropic client constructor API


grep -n 'MessageNewParams\|type Message\b\|Content\[' /Users/steve.briggs/go/pkg/mod/github.com/anthropics/anthropic-sdk-go@v1.37.0/message.go 2>/dev/null | head -30

Check Anthropic message params and response types


21:type Client struct {
50:func NewClient(opts ...option.RequestOption) (r Client) {


55:func (r *MessageService) New(ctx context.Context, body MessageNewParams, opts ...option.RequestOption) (res *Message, err error) {
82:func (r *MessageService) NewStreaming(ctx context.Context, body MessageNewParams, opts ...option.RequestOption) (stream *ssestream.Stream[MessageStreamEventUnion]) {
3339:type Message struct {
9105:type MessageNewParams struct {
9234:	ServiceTier MessageNewParamsServiceTier `json:"service_tier,omitzero"`
9349:func (r MessageNewParams) MarshalJSON() (data []byte, err error) {
9350:	type shadow MessageNewParams
9353:func (r *MessageNewParams) UnmarshalJSON(data []byte) error {
9362:type MessageNewParamsServiceTier string
9365:	MessageNewParamsServiceTierAuto         MessageNewParamsServiceTier = "auto"
9366:	MessageNewParamsServiceTierStandardOnly MessageNewParamsServiceTier = "standard_only"


grep -n 'type Message struct\|Content \[\|ContentBlock\|\.Text\b' /Users/steve.briggs/go/pkg/mod/github.com/anthropics/anthropic-sdk-go@v1.37.0/message.go 2>/dev/null | head -30

Check Message struct and ContentBlock type


sed -n '3339,3380p' /Users/steve.briggs/go/pkg/mod/github.com/anthropics/anthropic-sdk-go@v1.37.0/message.go

Read Message struct definition


type Message struct {
	// Unique object identifier.
	//
	// The format and length of IDs may change over time.
	ID string `json:"id" api:"required"`
	// Information about the container used in the request (for the code execution
	// tool)
	Container Container `json:"container" api:"required"`
	// Content generated by the model.
	//
	// This is an array of content blocks, each of which has a `type` that determines
	// its shape.
	//
	// Example:
	//
	// ```json
	// [{ "type": "text", "text": "Hi, I'm Claude." }]
	// ```
	//
	// If the request input `messages` ended with an `assistant` turn, then the
	// response `content` will continue directly from that last turn. You can use this
	// to constrain the model's output.
	//
	// For example, if the input `messages` were:
	//
	// ```json
	// [
	//
	//	{
	//	  "role": "user",
	//	  "content": "What's the Greek name for Sun? (A) Sol (B) Helios (C) Sun"
	//	},
	//	{ "role": "assistant", "content": "The best answer is (" }
	//
	// ]
	// ```
	//
	// Then the response `content` might be:
	//
	// ```json
	// [{ "type": "text", "text": "B)" }]
	// ```


292:	Content []BashCodeExecutionOutputBlock `json:"content"`
587:type CitationContentBlockLocation struct {
594:	Type            constant.ContentBlockLocation `json:"type" default:"content_block_location"`
610:func (r CitationContentBlockLocation) RawJSON() string { return r.JSON.raw }
611:func (r *CitationContentBlockLocation) UnmarshalJSON(data []byte) error {
617:type CitationContentBlockLocationParam struct {
625:	Type constant.ContentBlockLocation `json:"type" default:"content_block_location"`
629:func (r CitationContentBlockLocationParam) MarshalJSON() (data []byte, err error) {
630:	type shadow CitationContentBlockLocationParam
633:func (r *CitationContentBlockLocationParam) UnmarshalJSON(data []byte) error {
777:// [CitationCharLocation], [CitationPageLocation], [CitationContentBlockLocation],
840:func (CitationContentBlockLocation) implCitationsDeltaCitationUnion()     {}
849://	case anthropic.CitationContentBlockLocation:
862:		return u.AsContentBlockLocation()
881:func (u CitationsDeltaCitationUnion) AsContentBlockLocation() (v CitationContentBlockLocation) {
1432:// ContentBlockUnion contains all possible properties and values from [TextBlock],
1439:// Use the [ContentBlockUnion.AsAny] method to switch on the variant.
1442:type ContentBlockUnion struct {
1484:// anyContentBlock is implemented by each variant of [ContentBlockUnion] to add
1485:// type safety for the return type of [ContentBlockUnion.AsAny]
1486:type anyContentBlock interface {
1487:	implContentBlockUnion()
1488:	toParamUnion() ContentBlockParamUnion
1491:func (TextBlock) implContentBlockUnion()                              {}
1492:func (ThinkingBlock) implContentBlockUnion()                          {}
1493:func (RedactedThinkingBlock) implContentBlockUnion()                  {}
1494:func (ToolUseBlock) implContentBlockUnion()                           {}
1495:func (ServerToolUseBlock) implContentBlockUnion()                     {}
1496:func (WebSearchToolResultBlock) implContentBlockUnion()               {}
1497:func (WebFetchToolResultBlock) implContentBlockUnion()                {}


sed -n '3380,3420p' /Users/steve.briggs/go/pkg/mod/github.com/anthropics/anthropic-sdk-go@v1.37.0/message.go

Read Content field of Message struct


	// ```
	Content []ContentBlockUnion `json:"content" api:"required"`
	// The model that will complete your prompt.\n\nSee
	// [models](https://docs.anthropic.com/en/docs/models-overview) for additional
	// details and options.
	Model Model `json:"model" api:"required"`
	// Conversational role of the generated message.
	//
	// This will always be `"assistant"`.
	Role constant.Assistant `json:"role" default:"assistant"`
	// Structured information about a refusal.
	StopDetails RefusalStopDetails `json:"stop_details" api:"required"`
	// The reason that we stopped.
	//
	// This may be one the following values:
	//
	//   - `"end_turn"`: the model reached a natural stopping point
	//   - `"max_tokens"`: we exceeded the requested `max_tokens` or the model's maximum
	//   - `"stop_sequence"`: one of your provided custom `stop_sequences` was generated
	//   - `"tool_use"`: the model invoked one or more tools
	//   - `"pause_turn"`: we paused a long-running turn. You may provide the response
	//     back as-is in a subsequent request to let the model continue.
	//   - `"refusal"`: when streaming classifiers intervene to handle potential policy
	//     violations
	//
	// In non-streaming mode this value is always non-null. In streaming mode, it is
	// null in the `message_start` event and non-null otherwise.
	//
	// Any of "end_turn", "max_tokens", "stop_sequence", "tool_use", "pause_turn",
	// "refusal".
	StopReason StopReason `json:"stop_reason" api:"required"`
	// Which custom stop sequence was generated, if any.
	//
	// This value will be a non-null string if one of your custom stop sequences was
	// generated.
	StopSequence string `json:"stop_sequence" api:"required"`
	// Object type.
	//
	// For Messages, this is always `"message"`.
	Type constant.Message `json:"type" default:"message"`
	// Billing and rate-limit usage.


grep -n 'type ContentBlockUnion\|func.*ContentBlockUnion\|\.Text\b' /Users/steve.briggs/go/pkg/mod/github.com/anthropics/anthropic-sdk-go@v1.37.0/message.go 2>/dev/null | head -20

Check ContentBlockUnion access patterns


1442:type ContentBlockUnion struct {
1491:func (TextBlock) implContentBlockUnion()                              {}
1492:func (ThinkingBlock) implContentBlockUnion()                          {}
1493:func (RedactedThinkingBlock) implContentBlockUnion()                  {}
1494:func (ToolUseBlock) implContentBlockUnion()                           {}
1495:func (ServerToolUseBlock) implContentBlockUnion()                     {}
1496:func (WebSearchToolResultBlock) implContentBlockUnion()               {}
1497:func (WebFetchToolResultBlock) implContentBlockUnion()                {}
1498:func (CodeExecutionToolResultBlock) implContentBlockUnion()           {}
1499:func (BashCodeExecutionToolResultBlock) implContentBlockUnion()       {}
1500:func (TextEditorCodeExecutionToolResultBlock) implContentBlockUnion() {}
1501:func (ToolSearchToolResultBlock) implContentBlockUnion()              {}
1502:func (ContainerUploadBlock) implContentBlockUnion()                   {}
1522:func (u ContentBlockUnion) AsAny() anyContentBlock {
1552:func (u ContentBlockUnion) AsText() (v TextBlock) {
1557:func (u ContentBlockUnion) AsThinking() (v ThinkingBlock) {
1562:func (u ContentBlockUnion) AsRedactedThinking() (v RedactedThinkingBlock) {
1567:func (u ContentBlockUnion) AsToolUse() (v ToolUseBlock) {
1572:func (u ContentBlockUnion) AsServerToolUse() (v ServerToolUseBlock) {
1577:func (u ContentBlockUnion) AsWebSearchToolResult() (v WebSearchToolResultBlock) {


grep -n 'type TextBlock struct\|Text ' /Users/steve.briggs/go/pkg/mod/github.com/anthropics/anthropic-sdk-go@v1.37.0/message.go 2>/dev/null | head -20

Check TextBlock struct


539:	CitedText      string                `json:"cited_text" api:"required"`
548:		CitedText      respjson.Field
570:	CitedText      string            `json:"cited_text" api:"required"`
588:	CitedText       string                        `json:"cited_text" api:"required"`
597:		CitedText       respjson.Field
619:	CitedText       string            `json:"cited_text" api:"required"`
638:	CitedText       string                `json:"cited_text" api:"required"`
647:		CitedText       respjson.Field
669:	CitedText       string            `json:"cited_text" api:"required"`
690:	CitedText         string            `json:"cited_text" api:"required"`
712:	CitedText      string            `json:"cited_text" api:"required"`
784:	CitedText     string `json:"cited_text"`
811:		CitedText         respjson.Field
904:	CitedText         string                        `json:"cited_text" api:"required"`
913:		CitedText         respjson.Field
932:	CitedText      string                           `json:"cited_text" api:"required"`
939:		CitedText      respjson.Field
1446:	Text string `json:"text"`
1468:		Text      respjson.Field
1760:	variant.Text = text


grep -n 'func NewUserMessage\|func NewTextBlock\|NewTextBlockParam\|type MessageParam' /Users/steve.briggs/go/pkg/mod/github.com/anthropics/anthropic-sdk-go@v1.37.0/message.go 2>/dev/null | head -20

Check message helper constructors


1758:func NewTextBlock(text string) ContentBlockParamUnion {
3963:type MessageParam struct {
3970:func NewUserMessage(blocks ...ContentBlockParamUnion) MessageParam {
3992:type MessageParamRole string


sed -n '9105,9200p' /Users/steve.briggs/go/pkg/mod/github.com/anthropics/anthropic-sdk-go@v1.37.0/message.go

Read MessageNewParams struct definition


type MessageNewParams struct {
	// The maximum number of tokens to generate before stopping.
	//
	// Note that our models may stop _before_ reaching this maximum. This parameter
	// only specifies the absolute maximum number of tokens to generate.
	//
	// Different models have different maximum values for this parameter. See
	// [models](https://docs.claude.com/en/docs/models-overview) for details.
	MaxTokens int64 `json:"max_tokens" api:"required"`
	// Input messages.
	//
	// Our models are trained to operate on alternating `user` and `assistant`
	// conversational turns. When creating a new `Message`, you specify the prior
	// conversational turns with the `messages` parameter, and the model then generates
	// the next `Message` in the conversation. Consecutive `user` or `assistant` turns
	// in your request will be combined into a single turn.
	//
	// Each input message must be an object with a `role` and `content`. You can
	// specify a single `user`-role message, or you can include multiple `user` and
	// `assistant` messages.
	//
	// If the final message uses the `assistant` role, the response content will
	// continue immediately from the content in that message. This can be used to
	// constrain part of the model's response.
	//
	// Example with a single `user` message:
	//
	// ```json
	// [{ "role": "user", "content": "Hello, Claude" }]
	// ```
	//
	// Example with multiple conversational turns:
	//
	// ```json
	// [
	//
	//	{ "role": "user", "content": "Hello there." },
	//	{ "role": "assistant", "content": "Hi, I'm Claude. How can I help you?" },
	//	{ "role": "user", "content": "Can you explain LLMs in plain English?" }
	//
	// ]
	// ```
	//
	// Example with a partially-filled response from Claude:
	//
	// ```json
	// [
	//
	//	{
	//	  "role": "user",
	//	  "content": "What's the Greek name for Sun? (A) Sol (B) Helios (C) Sun"
	//	},
	//	{ "role": "assistant", "content": "The best answer is (" }
	//
	// ]
	// ```
	//
	// Each input message `content` may be either a single `string` or an array of
	// content blocks, where each block has a specific `type`. Using a `string` for
	// `content` is shorthand for an array of one content block of type `"text"`. The
	// following input messages are equivalent:
	//
	// ```json
	// { "role": "user", "content": "Hello, Claude" }
	// ```
	//
	// ```json
	// { "role": "user", "content": [{ "type": "text", "text": "Hello, Claude" }] }
	// ```
	//
	// See [input examples](https://docs.claude.com/en/api/messages-examples).
	//
	// Note that if you want to include a
	// [system prompt](https://docs.claude.com/en/docs/system-prompts), you can use the
	// top-level `system` parameter — there is no `"system"` role for input messages in
	// the Messages API.
	//
	// There is a limit of 100,000 messages in a single request.
	Messages []MessageParam `json:"messages,omitzero" api:"required"`
	// The model that will complete your prompt.\n\nSee
	// [models](https://docs.anthropic.com/en/docs/models-overview) for additional
	// details and options.
	Model Model `json:"model,omitzero" api:"required"`
	// Container identifier for reuse across requests.
	Container param.Opt[string] `json:"container,omitzero"`
	// Specifies the geographic region for inference processing. If not specified, the
	// workspace's `default_inference_geo` is used.
	InferenceGeo param.Opt[string] `json:"inference_geo,omitzero"`
	// Amount of randomness injected into the response.
	//
	// Defaults to `1.0`. Ranges from `0.0` to `1.0`. Use `temperature` closer to `0.0`
	// for analytical / multiple choice, and closer to `1.0` for creative and
	// generative tasks.
	//
	// Note that even with `temperature` of `0.0`, the results will not be fully
	// deterministic.


grep -n 'System\b' /Users/steve.briggs/go/pkg/mod/github.com/anthropics/anthropic-sdk-go@v1.37.0/message.go | grep -i 'param\|field\|system' | head -20

Find System field in MessageNewParams


9245:	// System prompt.
9250:	System []TextBlockParam `json:"system,omitzero"`
9449:	// System prompt.
9454:	System MessageCountTokensParamsSystemUnion `json:"system,omitzero"`


grep -n 'type TextBlockParam\|func NewTextBlockParam\|Text \b' /Users/steve.briggs/go/pkg/mod/github.com/anthropics/anthropic-sdk-go@v1.37.0/message.go | head -20

Check TextBlockParam type


1446:	Text string `json:"text"`
4162:	Text string `json:"text"`
4313:	Text string `json:"text"`
4879:	Text string `json:"text"`
5376:type TextBlockParam struct {
5721:	Text string             `json:"text" api:"required"`


sed -n '5376,5420p' /Users/steve.briggs/go/pkg/mod/github.com/anthropics/anthropic-sdk-go@v1.37.0/message.go

Read TextBlockParam struct definition


type TextBlockParam struct {
	Text      string                   `json:"text" api:"required"`
	Citations []TextCitationParamUnion `json:"citations,omitzero"`
	// Create a cache control breakpoint at this content block.
	CacheControl CacheControlEphemeralParam `json:"cache_control,omitzero"`
	// This field can be elided, and will marshal its zero value as "text".
	Type constant.Text `json:"type" default:"text"`
	paramObj
}

func (r TextBlockParam) MarshalJSON() (data []byte, err error) {
	type shadow TextBlockParam
	return param.MarshalObject(r, (*shadow)(&r))
}
func (r *TextBlockParam) UnmarshalJSON(data []byte) error {
	return apijson.UnmarshalRoot(data, r)
}

// TextCitationUnion contains all possible properties and values from
// [CitationCharLocation], [CitationPageLocation], [CitationContentBlockLocation],
// [CitationsWebSearchResultLocation], [CitationsSearchResultLocation].
//
// Use the [TextCitationUnion.AsAny] method to switch on the variant.
//
// Use the methods beginning with 'As' to cast the union to one of its variants.
type TextCitationUnion struct {
	CitedText     string `json:"cited_text"`
	DocumentIndex int64  `json:"document_index"`
	DocumentTitle string `json:"document_title"`
	// This field is from variant [CitationCharLocation].
	EndCharIndex int64  `json:"end_char_index"`
	FileID       string `json:"file_id"`
	// This field is from variant [CitationCharLocation].
	StartCharIndex int64 `json:"start_char_index"`
	// Any of "char_location", "page_location", "content_block_location",
	// "web_search_result_location", "search_result_location".
	Type string `json:"type"`
	// This field is from variant [CitationPageLocation].
	EndPageNumber int64 `json:"end_page_number"`
	// This field is from variant [CitationPageLocation].
	StartPageNumber int64 `json:"start_page_number"`
	EndBlockIndex   int64 `json:"end_block_index"`
	StartBlockIndex int64 `json:"start_block_index"`
	// This field is from variant [CitationsWebSearchResultLocation].
	EncryptedIndex string `json:"encrypted_index"`


Now I have the exact API. The v1.37.0 SDK dropped the `F()` wrappers — fields are direct types. Let me fix the evaluator:


grep -n 'type Model\b\|ModelClaude' /Users/steve.briggs/go/pkg/mod/github.com/anthropics/anthropic-sdk-go@v1.37.0/aliases.go 2>/dev/null | head -10

Check Model type and constants


(Bash completed with no output)


grep -rn 'type Model\b' /Users/steve.briggs/go/pkg/mod/github.com/anthropics/anthropic-sdk-go@v1.37.0/ 2>/dev/null | head -5

Find Model type definition


/Users/steve.briggs/go/pkg/mod/github.com/anthropics/anthropic-sdk-go@v1.37.0/message.go:4038:type Model = string
/Users/steve.briggs/go/pkg/mod/github.com/anthropics/anthropic-sdk-go@v1.37.0/shared/constant/constants.go:119:type Model string                                   // Always "model"


Edit

	msg, err := e.client.Messages.New(ctx, anthropic.MessageNewParams{
		Model:     anthropic.F(anthropic.Model(e.model)),
		MaxTokens: anthropic.F(int64(512)),
		Messages: anthropic.F([]anthropic.MessageParam{
			anthropic.NewUserMessage(anthropic.NewTextBlock(prompt)),
		}),
	})
	if err != nil {
		return nil, fmt.Errorf("claude API: %w", err)
	}

	if len(msg.Content) == 0 {
		return nil, fmt.Errorf("empty response from claude")
	}

	text := strings.TrimSpace(msg.Content[0].Text)

	msg, err := e.client.Messages.New(ctx, anthropic.MessageNewParams{
		Model:     e.model,
		MaxTokens: 512,
		System: []anthropic.TextBlockParam{
			{Text: "You are a security risk evaluator for Teleport access requests. Respond only with valid JSON."},
		},
		Messages: []anthropic.MessageParam{
			anthropic.NewUserMessage(anthropic.NewTextBlock(prompt)),
		},
	})
	if err != nil {
		return nil, fmt.Errorf("claude API: %w", err)
	}

	if len(msg.Content) == 0 {
		return nil, fmt.Errorf("empty response from claude")
	}

	text := strings.TrimSpace(msg.Content[0].Text)


The file /Users/steve.briggs/projects/operations/services/televibe/internal/evaluator/evaluator.go has been updated successfully.

	msg, err := e.client.Messages.New(ctx, anthropic.MessageNewParams{
		Model:     e.model,
		MaxTokens: 512,
		System: []anthropic.TextBlockParam{
			{Text: "You are a security risk evaluator for Teleport access requests. Respond only with valid JSON."},
		},
		Messages: []anthropic.MessageParam{
			anthropic.NewUserMessage(anthropic.NewTextBlock(prompt)),
		},
	})
	if err != nil {
		return nil, fmt.Errorf("claude API: %w", err)
	}

	if len(msg.Content) == 0 {
		return nil, fmt.Errorf("empty response from claude")
	}

	text := strings.TrimSpace(msg.Content[0].Text)

package evaluator

import (
	"context"
	"encoding/json"
	"fmt"
	"regexp"
	"strings"

	"github.com/anthropics/anthropic-sdk-go"
	"github.com/anthropics/anthropic-sdk-go/option"
)

// jiraPattern matches CUSTX-NNNN and ITSM-NNNN ticket references (case-insensitive).
var jiraPattern = regexp.MustCompile(`(?i)\b(CUSTX|ITSM)-\d+\b`)

// RiskLevel represents the severity of an access request.
type RiskLevel string

const (
	RiskLow    RiskLevel = "low"
	RiskMedium RiskLevel = "medium"
	RiskHigh   RiskLevel = "high"
)

// Assessment is the structured output of a Claude risk evaluation.
type Assessment struct {
	JIRATicket         string    `json:"jira_ticket"`
	HasJIRATicket      bool      `json:"has_jira_ticket"`
	DescriptionQuality string    `json:"description_quality"`
	RiskScore          int       `json:"risk_score"`
	RiskLevel          RiskLevel `json:"risk_level"`
	Reasoning          string    `json:"reasoning"`
}

// Request holds the access request fields that the evaluator needs.
type Request struct {
	ID     string
	User   string
	Roles  []string
	Reason string
}

// Evaluator calls Claude to assign a risk score to a Teleport access request.
type Evaluator struct {
	client *anthropic.Client
	model  string
}

func New(apiKey, model string) *Evaluator {
	return &Evaluator{
		client: anthropic.NewClient(option.WithAPIKey(apiKey)),
		model:  model,
	}
}

func (e *Evaluator) Evaluate(ctx context.Context, req Request) (*Assessment, error) {
	tickets := jiraPattern.FindAllString(req.Reason, -1)
	prompt := buildPrompt(req.User, req.Roles, req.Reason, tickets)

	msg, err := e.client.Messages.New(ctx, anthropic.MessageNewParams{
		Model:     anthropic.F(anthropic.Model(e.model)),
		MaxTokens: anthropic.F(int64(512)),
		Messages: anthropic.F([]anthropic.MessageParam{
			anthropic.NewUserMessage(anthropic.NewTextBlock(prompt)),
		}),
	})
	if err != nil {
		return nil, fmt.Errorf("claude API: %w", err)
	}

	if len(msg.Content) == 0 {
		return nil, fmt.Errorf("empty response from claude")
	}

	text := strings.TrimSpace(msg.Content[0].Text)
	// Strip markdown code fences that Claude sometimes adds.
	if strings.HasPrefix(text, "```") {
		if i := strings.Index(text[3:], "\n"); i >= 0 {
			text = strings.TrimSuffix(strings.TrimSpace(text[3+i+1:]), "```")
		}
	}

	var a Assessment
	if err := json.Unmarshal([]byte(text), &a); err != nil {
		return nil, fmt.Errorf("parsing response: %w (raw: %s)", err, text)
	}

	return &a, nil
}

func buildPrompt(user string, roles []string, reason string, tickets []string) string {
	ticketInfo := "none"
	if len(tickets) > 0 {
		ticketInfo = strings.Join(tickets, ", ")
	}

	return fmt.Sprintf(`You are a security risk evaluator for Teleport access requests. Respond ONLY with a valid JSON object — no explanation, no markdown.

Access request details:
  User:            %s
  Requested roles: %s
  Reason provided: %q
  JIRA tickets:    %s

Risk scoring rules:
  1–3  (low)    — valid JIRA ticket (CUSTX-NNN or ITSM-NNN) AND a clear description with business justification
  4–6  (medium) — either a valid JIRA ticket OR a clear description, but not both
  7–10 (high)   — no JIRA ticket AND no/vague description (single words or generic phrases like "need access" score 9–10)

Return exactly this JSON shape:
{
  "jira_ticket":          "<ticket ID or null>",
  "has_jira_ticket":      <true|false>,
  "description_quality":  "<none|poor|fair|good|excellent>",
  "risk_score":           <integer 1–10>,
  "risk_level":           "<low|medium|high>",
  "reasoning":            "<one concise sentence>"
}`,
		user,
		strings.Join(roles, ", "),
		reason,
		ticketInfo,
	)
}

-    Model:     anthropic.F(anthropic.Model(e.model)),

-    MaxTokens: anthropic.F(int64(512)),

-    Messages: anthropic.F([]anthropic.MessageParam{

+    System: []anthropic.TextBlockParam{

+      {Text: "You are a security risk evaluator for Teleport access requests. Respond only with valid JSON."},

+    Messages: []anthropic.MessageParam{


Now check if it compiles:


Bash

Build televibe to check for compilation errors


Error: Exit code 1
# github.com/immuta/operations/services/televibe/internal/evaluator
internal/evaluator/evaluator.go:52:11: cannot use anthropic.NewClient(option.WithAPIKey(apiKey)) (value of struct type anthropic.Client) as *anthropic.Client value in struct literal