Merge pull request '[agent/codex] Full audit per issue #4. Read CLAUDE.md. Report ALL findings...' (#5) from agent/deep-audit-per-issue--4--read-claude-md into dev
This commit is contained in:
commit
dfea9a6808
11 changed files with 128 additions and 70 deletions
17
CLAUDE.md
17
CLAUDE.md
|
|
@ -22,6 +22,12 @@ core go test --run Name # Single test
|
|||
core go fmt # Format
|
||||
core go lint # Lint
|
||||
core go vet # Vet
|
||||
core go qa # fmt + vet + lint + test
|
||||
```
|
||||
|
||||
If running `go` directly (outside `core`), set `GOWORK=off` to avoid workspace resolution errors:
|
||||
```bash
|
||||
GOWORK=off go test -cover ./...
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
|
@ -113,10 +119,17 @@ Backend packages use `var _ io.Medium = (*Medium)(nil)` to verify interface comp
|
|||
|
||||
- `forge.lthn.ai/Snider/Borg` — DataNode container
|
||||
- `forge.lthn.ai/core/go-log` — error handling (`coreerr.E()`)
|
||||
- `forge.lthn.ai/core/go/pkg/core` — Core DI (workspace service only)
|
||||
- `forge.lthn.ai/core/go` — Core DI (workspace service only)
|
||||
- `forge.lthn.ai/core/go-crypt` — PGP key generation (workspace service only)
|
||||
- `aws-sdk-go-v2` — S3 backend
|
||||
- `golang.org/x/crypto` — XChaCha20-Poly1305, BLAKE2, SHA-3 (sigil package)
|
||||
- `modernc.org/sqlite` — SQLite backends (pure Go, no CGO)
|
||||
- `github.com/stretchr/testify` — test assertions
|
||||
|
||||
### Sentinel Errors
|
||||
|
||||
Sentinel errors (`var ErrNotFound`, `var ErrInvalidKey`, etc.) use standard `errors.New()` — this is correct Go convention. Only inline error returns in functions should use `coreerr.E()`.
|
||||
|
||||
## Testing
|
||||
|
||||
All backends have full test coverage. Use `io.MockMedium` or `io.NewSandboxed(t.TempDir())` in tests — never hit real S3/SQLite unless integration testing.
|
||||
Use `io.MockMedium` or `io.NewSandboxed(t.TempDir())` in tests — never hit real S3/SQLite unless integration testing. S3 tests use an interface-based mock (`s3API`).
|
||||
|
|
|
|||
|
|
@ -135,6 +135,10 @@ func (m *Medium) Write(p, content string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (m *Medium) WriteMode(p, content string, mode os.FileMode) error {
|
||||
return m.Write(p, content)
|
||||
}
|
||||
|
||||
func (m *Medium) EnsureDir(p string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import (
|
|||
"io"
|
||||
"testing"
|
||||
|
||||
coreio "forge.lthn.ai/core/go-io"
|
||||
coreio "dappco.re/go/core/io"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
|
|||
7
go.mod
7
go.mod
|
|
@ -1,20 +1,21 @@
|
|||
module forge.lthn.ai/core/go-io
|
||||
module dappco.re/go/core/io
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
dappco.re/go/core v0.4.7
|
||||
forge.lthn.ai/Snider/Borg v0.3.1
|
||||
forge.lthn.ai/core/go v0.3.1
|
||||
forge.lthn.ai/core/go-crypt v0.1.6
|
||||
forge.lthn.ai/core/go-log v0.0.4
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.4
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.1
|
||||
github.com/stretchr/testify v1.11.1
|
||||
golang.org/x/crypto v0.49.0
|
||||
modernc.org/sqlite v1.46.2
|
||||
modernc.org/sqlite v1.47.0
|
||||
)
|
||||
|
||||
require (
|
||||
forge.lthn.ai/core/go v0.3.0 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.4.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 // indirect
|
||||
|
|
|
|||
10
go.sum
10
go.sum
|
|
@ -1,7 +1,9 @@
|
|||
dappco.re/go/core v0.4.7 h1:KmIA/2lo6rl1NMtLrKqCWfMlUqpDZYH3q0/d10dTtGA=
|
||||
dappco.re/go/core v0.4.7/go.mod h1:f2/tBZ3+3IqDrg2F5F598llv0nmb/4gJVCFzM5geE4A=
|
||||
forge.lthn.ai/Snider/Borg v0.3.1 h1:gfC1ZTpLoZai07oOWJiVeQ8+qJYK8A795tgVGJHbVL8=
|
||||
forge.lthn.ai/Snider/Borg v0.3.1/go.mod h1:Z7DJD0yHXsxSyM7Mjl6/g4gH1NBsIz44Bf5AFlV76Wg=
|
||||
forge.lthn.ai/core/go v0.3.1 h1:5FMTsUhLcxSr07F9q3uG0Goy4zq4eLivoqi8shSY4UM=
|
||||
forge.lthn.ai/core/go v0.3.1/go.mod h1:gE6c8h+PJ2287qNhVUJ5SOe1kopEwHEquvinstpuyJc=
|
||||
forge.lthn.ai/core/go v0.3.0 h1:mOG97ApMprwx9Ked62FdWVwXTGSF6JO6m0DrVpoH2Q4=
|
||||
forge.lthn.ai/core/go v0.3.0/go.mod h1:gE6c8h+PJ2287qNhVUJ5SOe1kopEwHEquvinstpuyJc=
|
||||
forge.lthn.ai/core/go-crypt v0.1.6 h1:jB7L/28S1NR+91u3GcOYuKfBLzPhhBUY1fRe6WkGVns=
|
||||
forge.lthn.ai/core/go-crypt v0.1.6/go.mod h1:4VZAGqxlbadhSB66sJkdj54/HSJ+bSxVgwWK5kMMYDo=
|
||||
forge.lthn.ai/core/go-log v0.0.4 h1:KTuCEPgFmuM8KJfnyQ8vPOU1Jg654W74h8IJvfQMfv0=
|
||||
|
|
@ -97,8 +99,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
|||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.46.2 h1:gkXQ6R0+AjxFC/fTDaeIVLbNLNrRoOK7YYVz5BKhTcE=
|
||||
modernc.org/sqlite v1.46.2/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
|
||||
modernc.org/sqlite v1.47.0 h1:R1XyaNpoW4Et9yly+I2EeX7pBza/w+pmYee/0HJDyKk=
|
||||
modernc.org/sqlite v1.47.0/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
|
|
|
|||
11
io.go
11
io.go
|
|
@ -9,7 +9,7 @@ import (
|
|||
"time"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
"forge.lthn.ai/core/go-io/local"
|
||||
"dappco.re/go/core/io/local"
|
||||
)
|
||||
|
||||
// Medium defines the standard interface for a storage backend.
|
||||
|
|
@ -20,8 +20,13 @@ type Medium interface {
|
|||
Read(path string) (string, error)
|
||||
|
||||
// Write saves the given content to a file, overwriting it if it exists.
|
||||
// Default permissions: 0644. For sensitive files, use WriteMode.
|
||||
Write(path, content string) error
|
||||
|
||||
// WriteMode saves content with explicit file permissions.
|
||||
// Use 0600 for sensitive files (keys, secrets, encrypted output).
|
||||
WriteMode(path, content string, mode os.FileMode) error
|
||||
|
||||
// EnsureDir makes sure a directory exists, creating it if necessary.
|
||||
EnsureDir(path string) error
|
||||
|
||||
|
|
@ -200,6 +205,10 @@ func (m *MockMedium) Write(path, content string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (m *MockMedium) WriteMode(path, content string, mode os.FileMode) error {
|
||||
return m.Write(path, content)
|
||||
}
|
||||
|
||||
// EnsureDir records that a directory exists in the mock filesystem.
|
||||
func (m *MockMedium) EnsureDir(path string) error {
|
||||
m.Dirs[path] = true
|
||||
|
|
|
|||
|
|
@ -124,7 +124,15 @@ func (m *Medium) Read(p string) (string, error) {
|
|||
}
|
||||
|
||||
// Write saves content to file, creating parent directories as needed.
|
||||
// Files are created with mode 0644. For sensitive files (keys, secrets),
|
||||
// use WriteMode with 0600.
|
||||
func (m *Medium) Write(p, content string) error {
|
||||
return m.WriteMode(p, content, 0644)
|
||||
}
|
||||
|
||||
// WriteMode saves content to file with explicit permissions.
|
||||
// Use 0600 for sensitive files (encryption output, private keys, auth hashes).
|
||||
func (m *Medium) WriteMode(p, content string, mode os.FileMode) error {
|
||||
full, err := m.validatePath(p)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -132,7 +140,7 @@ func (m *Medium) Write(p, content string) error {
|
|||
if err := os.MkdirAll(filepath.Dir(full), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(full, []byte(content), 0644)
|
||||
return os.WriteFile(full, []byte(content), mode)
|
||||
}
|
||||
|
||||
// EnsureDir creates directory if it doesn't exist.
|
||||
|
|
|
|||
10
node/node.go
10
node/node.go
|
|
@ -15,7 +15,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
coreio "forge.lthn.ai/core/go-io"
|
||||
coreio "dappco.re/go/core/io"
|
||||
)
|
||||
|
||||
// Node is an in-memory filesystem that implements coreio.Node (and therefore
|
||||
|
|
@ -361,6 +361,11 @@ func (n *Node) Write(p, content string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// WriteMode saves content with explicit permissions (no-op for in-memory node).
|
||||
func (n *Node) WriteMode(p, content string, mode os.FileMode) error {
|
||||
return n.Write(p, content)
|
||||
}
|
||||
|
||||
// FileGet is an alias for Read.
|
||||
func (n *Node) FileGet(p string) (string, error) {
|
||||
return n.Read(p)
|
||||
|
|
@ -607,6 +612,3 @@ var _ fs.File = (*dataFileReader)(nil)
|
|||
|
||||
// ensure all internal compile-time checks are grouped above
|
||||
// no further type assertions needed
|
||||
|
||||
// unused import guard
|
||||
var _ = os.ErrNotExist
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@ import (
|
|||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
"golang.org/x/crypto/blake2b"
|
||||
"golang.org/x/crypto/blake2s"
|
||||
"golang.org/x/crypto/md4"
|
||||
|
|
@ -204,7 +204,7 @@ func (s *HashSigil) In(data []byte) ([]byte, error) {
|
|||
h, _ = blake2b.New512(nil)
|
||||
default:
|
||||
// MD5SHA1 is not supported as a direct hash
|
||||
return nil, errors.New("sigil: hash algorithm not available")
|
||||
return nil, coreerr.E("sigil.HashSigil.In", "hash algorithm not available", nil)
|
||||
}
|
||||
|
||||
h.Write(data)
|
||||
|
|
@ -269,6 +269,6 @@ func NewSigil(name string) (Sigil, error) {
|
|||
case "blake2b-512":
|
||||
return NewHashSigil(crypto.BLAKE2b_512), nil
|
||||
default:
|
||||
return nil, errors.New("sigil: unknown sigil name")
|
||||
return nil, coreerr.E("sigil.NewSigil", "unknown sigil name: "+name, nil)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,14 +7,29 @@ import (
|
|||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
core "forge.lthn.ai/core/go/pkg/core"
|
||||
"forge.lthn.ai/core/go-io"
|
||||
|
||||
"dappco.re/go/core/io"
|
||||
)
|
||||
|
||||
// Service implements the core.Workspace interface.
|
||||
// Workspace provides management for encrypted user workspaces.
|
||||
type Workspace interface {
|
||||
CreateWorkspace(identifier, password string) (string, error)
|
||||
SwitchWorkspace(name string) error
|
||||
WorkspaceFileGet(filename string) (string, error)
|
||||
WorkspaceFileSet(filename, content string) error
|
||||
}
|
||||
|
||||
// cryptProvider is the interface for PGP key generation.
|
||||
type cryptProvider interface {
|
||||
CreateKeyPair(name, passphrase string) (string, error)
|
||||
}
|
||||
|
||||
// Service implements the Workspace interface.
|
||||
type Service struct {
|
||||
core *core.Core
|
||||
crypt cryptProvider
|
||||
activeWorkspace string
|
||||
rootPath string
|
||||
medium io.Medium
|
||||
|
|
@ -22,7 +37,8 @@ type Service struct {
|
|||
}
|
||||
|
||||
// New creates a new Workspace service instance.
|
||||
func New(c *core.Core) (any, error) {
|
||||
// An optional cryptProvider can be passed to supply PGP key generation.
|
||||
func New(c *core.Core, crypt ...cryptProvider) (any, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return nil, coreerr.E("workspace.New", "failed to determine home directory", err)
|
||||
|
|
@ -35,6 +51,10 @@ func New(c *core.Core) (any, error) {
|
|||
medium: io.Local,
|
||||
}
|
||||
|
||||
if len(crypt) > 0 && crypt[0] != nil {
|
||||
s.crypt = crypt[0]
|
||||
}
|
||||
|
||||
if err := s.medium.EnsureDir(rootPath); err != nil {
|
||||
return nil, coreerr.E("workspace.New", "failed to ensure root directory", err)
|
||||
}
|
||||
|
|
@ -43,13 +63,16 @@ func New(c *core.Core) (any, error) {
|
|||
}
|
||||
|
||||
// CreateWorkspace creates a new encrypted workspace.
|
||||
// Identifier is hashed (SHA-256 as proxy for LTHN) to create the directory name.
|
||||
// Identifier is hashed (SHA-256) to create the directory name.
|
||||
// A PGP keypair is generated using the password.
|
||||
func (s *Service) CreateWorkspace(identifier, password string) (string, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
// 1. Identification (LTHN hash proxy)
|
||||
if s.crypt == nil {
|
||||
return "", coreerr.E("workspace.CreateWorkspace", "crypt service not available", nil)
|
||||
}
|
||||
|
||||
hash := sha256.Sum256([]byte(identifier))
|
||||
wsID := hex.EncodeToString(hash[:])
|
||||
wsPath := filepath.Join(s.rootPath, wsID)
|
||||
|
|
@ -58,26 +81,18 @@ func (s *Service) CreateWorkspace(identifier, password string) (string, error) {
|
|||
return "", coreerr.E("workspace.CreateWorkspace", "workspace already exists", nil)
|
||||
}
|
||||
|
||||
// 2. Directory structure
|
||||
dirs := []string{"config", "log", "data", "files", "keys"}
|
||||
for _, d := range dirs {
|
||||
for _, d := range []string{"config", "log", "data", "files", "keys"} {
|
||||
if err := s.medium.EnsureDir(filepath.Join(wsPath, d)); err != nil {
|
||||
return "", coreerr.E("workspace.CreateWorkspace", "failed to create directory: "+d, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. PGP Keypair generation
|
||||
crypt := s.core.Crypt()
|
||||
if crypt == nil {
|
||||
return "", coreerr.E("workspace.CreateWorkspace", "crypt service not available", nil)
|
||||
}
|
||||
privKey, err := crypt.CreateKeyPair(identifier, password)
|
||||
privKey, err := s.crypt.CreateKeyPair(identifier, password)
|
||||
if err != nil {
|
||||
return "", coreerr.E("workspace.CreateWorkspace", "failed to generate keys", err)
|
||||
}
|
||||
|
||||
// Save private key
|
||||
if err := s.medium.Write(filepath.Join(wsPath, "keys", "private.key"), privKey); err != nil {
|
||||
if err := s.medium.WriteMode(filepath.Join(wsPath, "keys", "private.key"), privKey, 0600); err != nil {
|
||||
return "", coreerr.E("workspace.CreateWorkspace", "failed to save private key", err)
|
||||
}
|
||||
|
||||
|
|
@ -98,36 +113,41 @@ func (s *Service) SwitchWorkspace(name string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// activeFilePath returns the full path to a file in the active workspace,
|
||||
// or an error if no workspace is active.
|
||||
func (s *Service) activeFilePath(op, filename string) (string, error) {
|
||||
if s.activeWorkspace == "" {
|
||||
return "", coreerr.E(op, "no active workspace", nil)
|
||||
}
|
||||
return filepath.Join(s.rootPath, s.activeWorkspace, "files", filename), nil
|
||||
}
|
||||
|
||||
// WorkspaceFileGet retrieves the content of a file from the active workspace.
|
||||
// In a full implementation, this would involve decryption using the workspace key.
|
||||
func (s *Service) WorkspaceFileGet(filename string) (string, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
if s.activeWorkspace == "" {
|
||||
return "", coreerr.E("workspace.WorkspaceFileGet", "no active workspace", nil)
|
||||
path, err := s.activeFilePath("workspace.WorkspaceFileGet", filename)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
path := filepath.Join(s.rootPath, s.activeWorkspace, "files", filename)
|
||||
return s.medium.Read(path)
|
||||
}
|
||||
|
||||
// WorkspaceFileSet saves content to a file in the active workspace.
|
||||
// In a full implementation, this would involve encryption using the workspace key.
|
||||
func (s *Service) WorkspaceFileSet(filename, content string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.activeWorkspace == "" {
|
||||
return coreerr.E("workspace.WorkspaceFileSet", "no active workspace", nil)
|
||||
path, err := s.activeFilePath("workspace.WorkspaceFileSet", filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
path := filepath.Join(s.rootPath, s.activeWorkspace, "files", filename)
|
||||
return s.medium.Write(path, content)
|
||||
}
|
||||
|
||||
// HandleIPCEvents handles workspace-related IPC messages.
|
||||
func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) error {
|
||||
func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) core.Result {
|
||||
switch m := msg.(type) {
|
||||
case map[string]any:
|
||||
action, _ := m["action"].(string)
|
||||
|
|
@ -135,15 +155,21 @@ func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) error {
|
|||
case "workspace.create":
|
||||
id, _ := m["identifier"].(string)
|
||||
pass, _ := m["password"].(string)
|
||||
_, err := s.CreateWorkspace(id, pass)
|
||||
return err
|
||||
wsID, err := s.CreateWorkspace(id, pass)
|
||||
if err != nil {
|
||||
return core.Result{}
|
||||
}
|
||||
return core.Result{Value: wsID, OK: true}
|
||||
case "workspace.switch":
|
||||
name, _ := m["name"].(string)
|
||||
return s.SwitchWorkspace(name)
|
||||
if err := s.SwitchWorkspace(name); err != nil {
|
||||
return core.Result{}
|
||||
}
|
||||
return core.Result{OK: true}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return core.Result{OK: true}
|
||||
}
|
||||
|
||||
// Ensure Service implements core.Workspace.
|
||||
var _ core.Workspace = (*Service)(nil)
|
||||
// Ensure Service implements Workspace.
|
||||
var _ Workspace = (*Service)(nil)
|
||||
|
|
|
|||
|
|
@ -1,32 +1,25 @@
|
|||
package workspace
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"forge.lthn.ai/core/go-crypt/crypt/openpgp"
|
||||
core "forge.lthn.ai/core/go/pkg/core"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestWorkspace(t *testing.T) {
|
||||
// Setup core with crypt service
|
||||
c, _ := core.New(
|
||||
core.WithName("crypt", openpgp.New),
|
||||
)
|
||||
|
||||
tempHome, _ := os.MkdirTemp("", "core-test-home")
|
||||
defer os.RemoveAll(tempHome)
|
||||
|
||||
// Mock os.UserHomeDir by setting HOME env
|
||||
oldHome := os.Getenv("HOME")
|
||||
os.Setenv("HOME", tempHome)
|
||||
defer os.Setenv("HOME", oldHome)
|
||||
|
||||
s_any, err := New(c)
|
||||
c := core.New()
|
||||
pgpSvc, err := openpgp.New(nil)
|
||||
assert.NoError(t, err)
|
||||
s := s_any.(*Service)
|
||||
|
||||
tempHome := t.TempDir()
|
||||
t.Setenv("HOME", tempHome)
|
||||
|
||||
svc, err := New(c, pgpSvc.(cryptProvider))
|
||||
assert.NoError(t, err)
|
||||
s := svc.(*Service)
|
||||
|
||||
// Test CreateWorkspace
|
||||
id, err := s.CreateWorkspace("test-user", "pass123")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue