Forward Scriptorium credentials through the shared adapter path
This commit is contained in:
@@ -3,6 +3,7 @@ package scriptorium
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/fileops"
|
||||
)
|
||||
@@ -65,6 +66,7 @@ func (f *FakeRunner) RunArtifact(ctx context.Context, req RunArtifactRequest) (A
|
||||
if err := ctx.Err(); err != nil {
|
||||
return ArtifactResult{}, err
|
||||
}
|
||||
req.Credentials = slices.Clone(req.Credentials)
|
||||
f.RunRequests = append(f.RunRequests, req)
|
||||
if f.RunErr != nil {
|
||||
return ArtifactResult{}, f.RunErr
|
||||
@@ -105,6 +107,7 @@ func (f *FakeRunner) RenderArtifact(ctx context.Context, req RenderArtifactReque
|
||||
if err := ctx.Err(); err != nil {
|
||||
return ArtifactResult{}, err
|
||||
}
|
||||
req.Credentials = slices.Clone(req.Credentials)
|
||||
f.RenderRequests = append(f.RenderRequests, req)
|
||||
if f.RenderErr != nil {
|
||||
return ArtifactResult{}, f.RenderErr
|
||||
|
||||
@@ -4,6 +4,8 @@ package scriptorium
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -32,7 +34,7 @@ type RunArtifactRequest struct {
|
||||
StderrLogPath string
|
||||
GeneratedConfigPath string
|
||||
Timeout time.Duration
|
||||
APIKeyEnv string
|
||||
Credentials []configkit.Secret
|
||||
WorkingDir string
|
||||
}
|
||||
|
||||
@@ -49,7 +51,7 @@ type RenderArtifactRequest struct {
|
||||
StderrLogPath string
|
||||
GeneratedConfigPath string
|
||||
Timeout time.Duration
|
||||
APIKeyEnv string
|
||||
Credentials []configkit.Secret
|
||||
WorkingDir string
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,12 @@ func (r *SubprocessRunner) RunArtifact(ctx context.Context, req RunArtifactReque
|
||||
if r == nil {
|
||||
return ArtifactResult{}, fmt.Errorf("scriptorium subprocess runner is nil")
|
||||
}
|
||||
credentialPresent, err := validateCommonRunRequest(req.Binary, req.ConfigPath, req.PromptID, req.OutputPath, req.Timeout, req.APIKeyEnv, req.InputPaths, req.Vars)
|
||||
err := validateCommonRunRequest(req.Binary, req.ConfigPath, req.PromptID, req.OutputPath, req.Timeout, req.InputPaths, req.Vars)
|
||||
if err != nil {
|
||||
return ArtifactResult{}, err
|
||||
}
|
||||
|
||||
envOverrides, sensitiveNames, err := subprocess.CredentialEnvironment(req.Credentials)
|
||||
if err != nil {
|
||||
return ArtifactResult{}, err
|
||||
}
|
||||
@@ -46,8 +51,7 @@ func (r *SubprocessRunner) RunArtifact(ctx context.Context, req RunArtifactReque
|
||||
InputPaths: cloneStringMap(req.InputPaths),
|
||||
Vars: cloneStringMap(req.Vars),
|
||||
OutputPath: req.OutputPath,
|
||||
APIKeyEnv: strings.TrimSpace(req.APIKeyEnv),
|
||||
APIKeyEnvPresent: credentialPresent,
|
||||
CredentialNames: sensitiveNames,
|
||||
WorkingDir: req.WorkingDir,
|
||||
RenderFormat: "",
|
||||
RenderPromptStore: false,
|
||||
@@ -56,7 +60,6 @@ func (r *SubprocessRunner) RunArtifact(ctx context.Context, req RunArtifactReque
|
||||
}
|
||||
}
|
||||
|
||||
envOverrides, sensitiveNames := credentialEnvironment(req.APIKeyEnv)
|
||||
runRes, runErr := subprocess.Run(ctx, subprocess.RunRequest{
|
||||
Executable: req.Binary,
|
||||
Args: args,
|
||||
@@ -80,12 +83,11 @@ func (r *SubprocessRunner) RunArtifact(ctx context.Context, req RunArtifactReque
|
||||
PromptID: req.PromptID,
|
||||
ProfileID: req.ProfileID,
|
||||
Metadata: map[string]any{
|
||||
"adapter": "scriptorium_subprocess",
|
||||
"config_path": strings.TrimSpace(req.ConfigPath),
|
||||
"inputs_count": len(req.InputPaths),
|
||||
"vars_count": len(req.Vars),
|
||||
"api_key_env": strings.TrimSpace(req.APIKeyEnv),
|
||||
"api_key_env_present": credentialPresent,
|
||||
"adapter": "scriptorium_subprocess",
|
||||
"config_path": strings.TrimSpace(req.ConfigPath),
|
||||
"inputs_count": len(req.InputPaths),
|
||||
"vars_count": len(req.Vars),
|
||||
"credential_env_names": sensitiveNames,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -114,7 +116,12 @@ func (r *SubprocessRunner) RenderArtifact(ctx context.Context, req RenderArtifac
|
||||
if r == nil {
|
||||
return ArtifactResult{}, fmt.Errorf("scriptorium subprocess runner is nil")
|
||||
}
|
||||
credentialPresent, err := validateCommonRunRequest(req.Binary, req.ConfigPath, req.PromptID, req.OutputPath, req.Timeout, req.APIKeyEnv, req.InputPaths, req.Vars)
|
||||
err := validateCommonRunRequest(req.Binary, req.ConfigPath, req.PromptID, req.OutputPath, req.Timeout, req.InputPaths, req.Vars)
|
||||
if err != nil {
|
||||
return ArtifactResult{}, err
|
||||
}
|
||||
|
||||
envOverrides, sensitiveNames, err := subprocess.CredentialEnvironment(req.Credentials)
|
||||
if err != nil {
|
||||
return ArtifactResult{}, err
|
||||
}
|
||||
@@ -132,8 +139,7 @@ func (r *SubprocessRunner) RenderArtifact(ctx context.Context, req RenderArtifac
|
||||
InputPaths: cloneStringMap(req.InputPaths),
|
||||
Vars: cloneStringMap(req.Vars),
|
||||
OutputPath: req.OutputPath,
|
||||
APIKeyEnv: strings.TrimSpace(req.APIKeyEnv),
|
||||
APIKeyEnvPresent: credentialPresent,
|
||||
CredentialNames: sensitiveNames,
|
||||
WorkingDir: req.WorkingDir,
|
||||
RenderFormat: "json",
|
||||
RenderPromptStore: false,
|
||||
@@ -142,7 +148,6 @@ func (r *SubprocessRunner) RenderArtifact(ctx context.Context, req RenderArtifac
|
||||
}
|
||||
}
|
||||
|
||||
envOverrides, sensitiveNames := credentialEnvironment(req.APIKeyEnv)
|
||||
runRes, runErr := subprocess.Run(ctx, subprocess.RunRequest{
|
||||
Executable: req.Binary,
|
||||
Args: args,
|
||||
@@ -166,13 +171,12 @@ func (r *SubprocessRunner) RenderArtifact(ctx context.Context, req RenderArtifac
|
||||
PromptID: req.PromptID,
|
||||
ProfileID: req.ProfileID,
|
||||
Metadata: map[string]any{
|
||||
"adapter": "scriptorium_subprocess",
|
||||
"config_path": strings.TrimSpace(req.ConfigPath),
|
||||
"inputs_count": len(req.InputPaths),
|
||||
"vars_count": len(req.Vars),
|
||||
"api_key_env": strings.TrimSpace(req.APIKeyEnv),
|
||||
"api_key_env_present": credentialPresent,
|
||||
"format": "json",
|
||||
"adapter": "scriptorium_subprocess",
|
||||
"config_path": strings.TrimSpace(req.ConfigPath),
|
||||
"inputs_count": len(req.InputPaths),
|
||||
"vars_count": len(req.Vars),
|
||||
"credential_env_names": sensitiveNames,
|
||||
"format": "json",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -191,57 +195,39 @@ func validateCommonRunRequest(
|
||||
promptID string,
|
||||
outputPath string,
|
||||
timeout time.Duration,
|
||||
apiKeyEnv string,
|
||||
inputPaths map[string]string,
|
||||
vars map[string]string,
|
||||
) (bool, error) {
|
||||
) error {
|
||||
if strings.TrimSpace(binary) == "" {
|
||||
return false, fmt.Errorf("scriptorium binary is required")
|
||||
return fmt.Errorf("scriptorium binary is required")
|
||||
}
|
||||
if strings.TrimSpace(configPath) == "" && configPath != "" {
|
||||
return false, fmt.Errorf("scriptorium config path must be non-empty when provided")
|
||||
return fmt.Errorf("scriptorium config path must be non-empty when provided")
|
||||
}
|
||||
if strings.TrimSpace(promptID) == "" {
|
||||
return false, fmt.Errorf("scriptorium prompt id is required")
|
||||
return fmt.Errorf("scriptorium prompt id is required")
|
||||
}
|
||||
if strings.TrimSpace(outputPath) == "" {
|
||||
return false, fmt.Errorf("scriptorium output path is required")
|
||||
return fmt.Errorf("scriptorium output path is required")
|
||||
}
|
||||
if timeout <= 0 {
|
||||
return false, fmt.Errorf("scriptorium timeout must be > 0")
|
||||
return fmt.Errorf("scriptorium timeout must be > 0")
|
||||
}
|
||||
for name, path := range inputPaths {
|
||||
if strings.TrimSpace(name) == "" {
|
||||
return false, fmt.Errorf("scriptorium input names must be non-empty")
|
||||
return fmt.Errorf("scriptorium input names must be non-empty")
|
||||
}
|
||||
if strings.TrimSpace(path) == "" {
|
||||
return false, fmt.Errorf("scriptorium input %q path is required", name)
|
||||
return fmt.Errorf("scriptorium input %q path is required", name)
|
||||
}
|
||||
}
|
||||
for name := range vars {
|
||||
if strings.TrimSpace(name) == "" {
|
||||
return false, fmt.Errorf("scriptorium variable names must be non-empty")
|
||||
return fmt.Errorf("scriptorium variable names must be non-empty")
|
||||
}
|
||||
}
|
||||
|
||||
trimmedEnv := strings.TrimSpace(apiKeyEnv)
|
||||
if trimmedEnv == "" {
|
||||
return false, nil
|
||||
}
|
||||
value, ok := os.LookupEnv(trimmedEnv)
|
||||
if !ok || strings.TrimSpace(value) == "" {
|
||||
return false, fmt.Errorf("scriptorium required credential environment variable %s is not set", trimmedEnv)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func credentialEnvironment(apiKeyEnv string) (map[string]string, []string) {
|
||||
name := strings.TrimSpace(apiKeyEnv)
|
||||
if name == "" {
|
||||
return nil, nil
|
||||
}
|
||||
value, _ := os.LookupEnv(name)
|
||||
return map[string]string{name: value}, []string{name}
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildRunArgs(req RunArtifactRequest) []string {
|
||||
@@ -258,9 +244,6 @@ func buildRunArgs(req RunArtifactRequest) []string {
|
||||
for _, kv := range sortedKeyValues(req.Vars) {
|
||||
args = append(args, "--var", kv)
|
||||
}
|
||||
if envName := strings.TrimSpace(req.APIKeyEnv); envName != "" {
|
||||
args = append(args, "--api-key-env", envName)
|
||||
}
|
||||
args = append(args, "--timeout", req.Timeout.String())
|
||||
args = append(args, "--out", req.OutputPath)
|
||||
return args
|
||||
@@ -280,9 +263,6 @@ func buildRenderArgs(req RenderArtifactRequest) []string {
|
||||
for _, kv := range sortedKeyValues(req.Vars) {
|
||||
args = append(args, "--var", kv)
|
||||
}
|
||||
if envName := strings.TrimSpace(req.APIKeyEnv); envName != "" {
|
||||
args = append(args, "--api-key-env", envName)
|
||||
}
|
||||
args = append(args, "--timeout", req.Timeout.String())
|
||||
args = append(args, "--format", "json")
|
||||
args = append(args, "--out", req.OutputPath)
|
||||
@@ -316,8 +296,7 @@ type invocationPayload struct {
|
||||
InputPaths map[string]string
|
||||
Vars map[string]string
|
||||
OutputPath string
|
||||
APIKeyEnv string
|
||||
APIKeyEnvPresent bool
|
||||
CredentialNames []string
|
||||
WorkingDir string
|
||||
RenderFormat string
|
||||
RenderPromptStore bool
|
||||
@@ -336,8 +315,7 @@ func writeInvocationConfig(path string, payload invocationPayload) error {
|
||||
"input_paths": payload.InputPaths,
|
||||
"vars": payload.Vars,
|
||||
"output_path": payload.OutputPath,
|
||||
"api_key_env": payload.APIKeyEnv,
|
||||
"api_key_env_present": payload.APIKeyEnvPresent,
|
||||
"credential_env_names": payload.CredentialNames,
|
||||
"working_dir": payload.WorkingDir,
|
||||
"render_format": payload.RenderFormat,
|
||||
"render_prompt_logged": payload.RenderPromptStore,
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
)
|
||||
|
||||
func TestSubprocessRunnerRunSuccessBuildsDeterministicArgsAndCapturesLogs(t *testing.T) {
|
||||
@@ -16,6 +18,13 @@ func TestSubprocessRunnerRunSuccessBuildsDeterministicArgsAndCapturesLogs(t *tes
|
||||
t.Setenv("GO_WANT_SCRIPTORIUM_HELPER", "1")
|
||||
t.Setenv("SCRIPTORIUM_HELPER_MODE", "run_success")
|
||||
t.Setenv("OPENAI_KEY_SOURCE", "super-secret")
|
||||
loaded, err := configkit.Load[struct {
|
||||
Credentials []configkit.Secret `yaml:"credentials"`
|
||||
}](configkit.Bytes("test.yml", []byte("credentials: [OPENAI_KEY_SOURCE]\n")))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("OPENAI_KEY_SOURCE", "changed-after-load")
|
||||
recordPath := filepath.Join(t.TempDir(), "record.json")
|
||||
t.Setenv("SCRIPTORIUM_HELPER_RECORD_PATH", recordPath)
|
||||
|
||||
@@ -34,7 +43,7 @@ func TestSubprocessRunnerRunSuccessBuildsDeterministicArgsAndCapturesLogs(t *tes
|
||||
StderrLogPath: filepath.Join(dir, "logs", "scriptorium.run.stderr.log"),
|
||||
GeneratedConfigPath: filepath.Join(dir, "config", "scriptorium.run.generated.yml"),
|
||||
Timeout: mustParseScriptoriumDuration(t, "2s"),
|
||||
APIKeyEnv: "OPENAI_KEY_SOURCE",
|
||||
Credentials: loaded.Config.Credentials,
|
||||
}
|
||||
writeScriptoriumFile(t, req.InputPaths["transcript"], `{"segments":[]}`)
|
||||
writeScriptoriumFile(t, req.InputPaths["other"], "notes\n")
|
||||
@@ -70,7 +79,6 @@ func TestSubprocessRunnerRunSuccessBuildsDeterministicArgsAndCapturesLogs(t *tes
|
||||
"--input", "transcript=" + req.InputPaths["transcript"],
|
||||
"--var", "campaign_name=Icewind Dale",
|
||||
"--var", "session_id=2026-05-03",
|
||||
"--api-key-env", "OPENAI_KEY_SOURCE",
|
||||
"--timeout", "2s",
|
||||
"--out", req.OutputPath,
|
||||
}
|
||||
@@ -94,13 +102,31 @@ func TestSubprocessRunnerRunExitCodeOneFails(t *testing.T) {
|
||||
t.Setenv("GO_WANT_SCRIPTORIUM_HELPER", "1")
|
||||
t.Setenv("SCRIPTORIUM_HELPER_MODE", "fail_exit1")
|
||||
t.Setenv("SCRIPTORIUM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json"))
|
||||
t.Setenv("CUSTOM_PROVIDER_KEY", "synthetic-failure-secret")
|
||||
loaded, loadErr := configkit.Load[struct {
|
||||
Credentials []configkit.Secret `yaml:"credentials"`
|
||||
}](configkit.Bytes("test.yml", []byte("credentials: [CUSTOM_PROVIDER_KEY]\n")))
|
||||
if loadErr != nil {
|
||||
t.Fatal(loadErr)
|
||||
}
|
||||
|
||||
runner := NewSubprocessRunner()
|
||||
req := runReqForTest(t, writeScriptoriumHelperWrapper(t))
|
||||
req.Credentials = loaded.Config.Credentials
|
||||
res, err := runner.RunArtifact(context.Background(), req)
|
||||
if err == nil {
|
||||
t.Fatal("RunArtifact() error = nil, want non-nil")
|
||||
}
|
||||
if strings.Contains(err.Error(), "synthetic-failure-secret") {
|
||||
t.Fatal("failure leaked credential")
|
||||
}
|
||||
data, readErr := os.ReadFile(req.StderrLogPath)
|
||||
if readErr != nil {
|
||||
t.Fatal(readErr)
|
||||
}
|
||||
if strings.Contains(string(data), "synthetic-failure-secret") || !strings.Contains(string(data), "<redacted>") {
|
||||
t.Fatal("failure log did not redact selected credential")
|
||||
}
|
||||
if res.ExitCode != 1 {
|
||||
t.Fatalf("ExitCode = %d, want 1", res.ExitCode)
|
||||
}
|
||||
@@ -245,6 +271,7 @@ func TestScriptoriumSubprocessHelper(t *testing.T) {
|
||||
os.Exit(0)
|
||||
case "fail_exit1":
|
||||
_, _ = os.Stderr.WriteString("scriptorium helper failure\n")
|
||||
_, _ = os.Stderr.WriteString(os.Getenv("CUSTOM_PROVIDER_KEY"))
|
||||
os.Exit(1)
|
||||
case "fail_exit2_with_output":
|
||||
writeScriptoriumHelperFile(outputPath, "validation failed artifact\n")
|
||||
|
||||
Reference in New Issue
Block a user