Harden CLI local vault spool durability

This commit is contained in:
Codex
2026-06-17 15:32:41 +02:00
parent 76349a6b55
commit f0605f1c3f
2 changed files with 70 additions and 2 deletions
+22 -2
View File
@@ -1051,6 +1051,12 @@ func (a *app) localVaultSpool(args []string) error {
if err := fs.Parse(args); err != nil { if err := fs.Parse(args); err != nil {
return err return err
} }
if strings.TrimSpace(*spoolFile) == "" {
return errors.New("--spool-file is required")
}
if strings.TrimSpace(*file) == "" {
return errors.New("--file is required")
}
raw, err := os.ReadFile(*file) raw, err := os.ReadFile(*file)
if err != nil { if err != nil {
return err return err
@@ -1066,12 +1072,26 @@ func (a *app) localVaultSpool(args []string) error {
if err != nil { if err != nil {
return err return err
} }
defer fh.Close()
canonical, err := attesto.CanonicalJSON(obj) canonical, err := attesto.CanonicalJSON(obj)
if err != nil { if err != nil {
_ = fh.Close()
return err return err
} }
if _, err := fh.Write(append(canonical, '\n')); err != nil { line := append(canonical, '\n')
written, err := fh.Write(line)
if err != nil {
_ = fh.Close()
return err
}
if written != len(line) {
_ = fh.Close()
return fmt.Errorf("short spool write: wrote %d of %d bytes", written, len(line))
}
if err := fh.Sync(); err != nil {
_ = fh.Close()
return err
}
if err := fh.Close(); err != nil {
return err return err
} }
return a.write(map[string]any{"ok": true, "spoolFile": *spoolFile}) return a.write(map[string]any{"ok": true, "spoolFile": *spoolFile})
+48
View File
@@ -158,6 +158,54 @@ func TestConfigSetRedactsSecrets(t *testing.T) {
} }
} }
func TestLocalVaultSpoolAndStatus(t *testing.T) {
dir := t.TempDir()
eventFile := filepath.Join(dir, "event.json")
spoolFile := filepath.Join(dir, "spool", "events.jsonl")
if err := os.WriteFile(eventFile, []byte(`{"z":2,"a":1}`), 0o600); err != nil {
t.Fatal(err)
}
var stdout, stderr bytes.Buffer
code := run([]string{"--json", "local-vault", "spool", "--spool-file", spoolFile, "--file", eventFile}, &stdout, &stderr, testEnv(t, nil))
if code != 0 {
t.Fatalf("exit=%d stderr=%s", code, stderr.String())
}
raw, err := os.ReadFile(spoolFile)
if err != nil {
t.Fatal(err)
}
if string(raw) != "{\"a\":1,\"z\":2}\n" {
t.Fatalf("spool must write canonical JSONL, got %q", raw)
}
info, err := os.Stat(spoolFile)
if err != nil {
t.Fatal(err)
}
if info.Mode().Perm() != 0o600 {
t.Fatalf("spool mode = %o, want 600", info.Mode().Perm())
}
stdout.Reset()
stderr.Reset()
code = run([]string{"--json", "local-vault", "status", "--spool-file", spoolFile}, &stdout, &stderr, testEnv(t, nil))
if code != 0 {
t.Fatalf("exit=%d stderr=%s", code, stderr.String())
}
if !strings.Contains(stdout.String(), `"events": 1`) || !strings.Contains(stdout.String(), `"ok": true`) {
t.Fatalf("unexpected status output: %s", stdout.String())
}
}
func TestLocalVaultSpoolRequiresPaths(t *testing.T) {
var stdout, stderr bytes.Buffer
code := run([]string{"--json", "local-vault", "spool", "--spool-file", filepath.Join(t.TempDir(), "events.jsonl")}, &stdout, &stderr, testEnv(t, nil))
if code == 0 {
t.Fatal("missing --file must fail")
}
if !strings.Contains(stderr.String(), "--file is required") {
t.Fatalf("missing required path error: %s", stderr.String())
}
}
func TestStreamsCreateCallsAPI(t *testing.T) { func TestStreamsCreateCallsAPI(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v2/streams" || r.Method != http.MethodPost { if r.URL.Path != "/v2/streams" || r.Method != http.MethodPost {