go/README.md
Snider b553afaf87
feat: CI improvements and release channels (#266)
* feat(help): Add CLI help command

Fixes #136

* chore: remove binary

* feat(mcp): Add TCP transport

Fixes #126

* feat(io): Migrate pkg/mcp to use Medium abstraction

Fixes #103

* feat(io): batch implementation placeholder

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(errors): batch implementation placeholder

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(log): batch implementation placeholder

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore(io): Migrate internal/cmd/docs/* to Medium abstraction

Fixes #113

* chore(io): Migrate internal/cmd/dev/* to Medium abstraction

Fixes #114

* chore(io): Migrate internal/cmd/setup/* to Medium abstraction

* chore(io): Complete migration of internal/cmd/dev/* to Medium abstraction

* feat(io): extend Medium interface with Delete, Rename, List, Stat operations

Adds the following methods to the Medium interface:
- Delete(path) - remove a file or empty directory
- DeleteAll(path) - recursively remove a file or directory
- Rename(old, new) - move/rename a file or directory
- List(path) - list directory entries (returns []fs.DirEntry)
- Stat(path) - get file information (returns fs.FileInfo)
- Exists(path) - check if path exists
- IsDir(path) - check if path is a directory

Implements these methods in both local.Medium (using os package)
and MockMedium (in-memory for testing). Includes FileInfo and
DirEntry types for mock implementations.

This enables migration of direct os.* calls to the Medium
abstraction for consistent path validation and testability.

Refs #101

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore(io): Migrate internal/cmd/sdk, pkgcmd, and workspace to Medium abstraction

* chore(io): migrate internal/cmd/docs and internal/cmd/dev to Medium

- internal/cmd/docs: Replace os.Stat, os.ReadFile, os.WriteFile,
  os.MkdirAll, os.RemoveAll with io.Local equivalents
- internal/cmd/dev: Replace os.Stat, os.ReadFile, os.WriteFile,
  os.MkdirAll, os.ReadDir with io.Local equivalents
- Fix local.Medium to allow absolute paths when root is "/" for
  full filesystem access (io.Local use case)

Refs #113, #114

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore(io): migrate internal/cmd/setup to Medium abstraction

Migrated all direct os.* filesystem calls to use io.Local:
- cmd_repo.go: os.MkdirAll -> io.Local.EnsureDir, os.WriteFile -> io.Local.Write, os.Stat -> io.Local.IsFile
- cmd_bootstrap.go: os.MkdirAll -> io.Local.EnsureDir, os.Stat -> io.Local.IsDir/Exists, os.ReadDir -> io.Local.List
- cmd_registry.go: os.MkdirAll -> io.Local.EnsureDir, os.Stat -> io.Local.Exists
- cmd_ci.go: os.ReadFile -> io.Local.Read
- github_config.go: os.ReadFile -> io.Local.Read, os.Stat -> io.Local.Exists

Refs #116

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(log): add error creation and log-and-return helpers

Implements issues #129 and #132:

- Add Err struct with Op, Msg, Err, Code fields for structured errors
- Add E(), Wrap(), WrapCode(), NewCode() for error creation
- Add Is(), As(), NewError(), Join() as stdlib wrappers
- Add Op(), ErrCode(), Message(), Root() for introspection
- Add LogError(), LogWarn(), Must() for combined log-and-return

Closes #129
Closes #132

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore(errors): create deprecation alias pointing to pkg/log

Makes pkg/errors a thin compatibility layer that re-exports from pkg/log.
All error handling functions now have canonical implementations in pkg/log.

Migration guide in package documentation:
- errors.Error -> log.Err
- errors.E -> log.E
- errors.Code -> log.NewCode
- errors.New -> log.NewError

Fixes behavior consistency:
- E(op, msg, nil) now creates an error (for errors without cause)
- Wrap(nil, op, msg) returns nil (for conditional wrapping)
- WrapCode returns nil only when both err is nil AND code is empty

Closes #128

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore(log): migrate pkg/errors imports to pkg/log

Migrates all internal packages from pkg/errors to pkg/log:
- internal/cmd/monitor
- internal/cmd/qa
- internal/cmd/dev
- pkg/agentic

Closes #130

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(io): address Copilot review feedback

- Fix MockMedium.Rename: collect keys before mutating maps during iteration
- Fix .git checks to use Exists instead of List (handles worktrees/submodules)
- Fix cmd_sync.go: use DeleteAll for recursive directory removal

Files updated:
- pkg/io/io.go: safe map iteration in Rename
- internal/cmd/setup/cmd_bootstrap.go: Exists for .git checks
- internal/cmd/setup/cmd_registry.go: Exists for .git checks
- internal/cmd/pkgcmd/cmd_install.go: Exists for .git checks
- internal/cmd/pkgcmd/cmd_manage.go: Exists for .git checks
- internal/cmd/docs/cmd_sync.go: DeleteAll for recursive delete

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(updater): resolve PkgVersion duplicate declaration

Remove var PkgVersion from updater.go since go generate creates
const PkgVersion in version.go. Track version.go in git to ensure
builds work without running go generate first.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* style: fix formatting in internal/variants

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* style: fix formatting across migrated files

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(io): simplify local Medium implementation

Rewrote to match the simpler TypeScript pattern:
- path() sanitizes and returns string directly
- Each method calls path() once
- No complex symlink validation
- Less code, less attack surface

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(io): remove duplicate method declarations

Clean up the client.go file that had duplicate method declarations
from a bad cherry-pick merge. Now has 127 lines of simple, clean code.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test(io): fix traversal test to match sanitization behavior

The simplified path() sanitizes .. to . without returning errors.
Update test to verify sanitization works correctly.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test(mcp): update sandboxing tests for simplified Medium

The simplified io/local.Medium implementation:
- Sanitizes .. to . (no error, path is cleaned)
- Allows absolute paths through (caller validates if needed)
- Follows symlinks (no traversal blocking)

Update tests to match this simplified behavior.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: address CodeRabbit review issues

- Fix critical sandbox escape in local.Medium.path()
  - Absolute paths now constrained to sandbox root when root != "/"
  - Only allow absolute path passthrough when root is "/"
- Fix weak test assertion in TestMust_Ugly_Panics
  - Use assert.Contains instead of weak OR condition
- Remove unused issues.json file
- Add TestPath_RootFilesystem test for absolute path handling

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(io): sandbox absolute paths under root in Medium.path

* ci(workflows): use host-uk/build@dev for releases

- Replace manual Go bootstrap with host-uk/build@dev action
- Add matrix builds for linux/amd64, linux/arm64, darwin/universal, windows/amd64
- Update README URLs from Snider/Core to host-uk/core
- Simplify artifact handling with merge-multiple

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(io): sandbox absolute paths under root in Medium.path

Security fix: Remove Windows drive root bypass and properly strip
volume names before sandboxing. Paths like C:\Windows are now
correctly sandboxed under root instead of escaping.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 03:52:09 +00:00

353 lines
11 KiB
Markdown

# Core
[![codecov](https://codecov.io/gh/host-uk/core/branch/dev/graph/badge.svg)](https://codecov.io/gh/host-uk/core)
[![Go Test Coverage](https://github.com/host-uk/core/actions/workflows/coverage.yml/badge.svg)](https://github.com/host-uk/core/actions/workflows/coverage.yml)
[![Code Scanning](https://github.com/host-uk/core/actions/workflows/codescan.yml/badge.svg)](https://github.com/host-uk/core/actions/workflows/codescan.yml)
[![Go Version](https://img.shields.io/github/go-mod/go-version/host-uk/core)](https://go.dev/)
[![License](https://img.shields.io/badge/License-EUPL--1.2-blue.svg)](https://opensource.org/licenses/EUPL-1.2)
Core is a Web3 Framework, written in Go using Wails.io to replace Electron and the bloat of browsers that, at their core, still live in their mum's basement.
- Repo: https://github.com/host-uk/core
## Vision
Core is an **opinionated Web3 desktop application framework** providing:
1. **Service-Oriented Architecture** - Pluggable services with dependency injection
2. **Encrypted Workspaces** - Each workspace gets its own PGP keypair, files are obfuscated
3. **Cross-Platform Storage** - Abstract storage backends (local, SFTP, WebDAV) behind a `Medium` interface
4. **Multi-Brand Support** - Same codebase powers different "hub" apps (AdminHub, ServerHub, GatewayHub, DeveloperHub, ClientHub)
5. **Built-in Crypto** - PGP encryption/signing, hashing, checksums as first-class citizens
**Mental model:** A secure, encrypted workspace manager where each "workspace" is a cryptographically isolated environment. The framework handles windows, menus, trays, config, and i18n.
## Quick Start
```go
import core "github.com/host-uk/core"
app := core.New(
core.WithServiceLock(),
)
```
## Prerequisites
- [Go](https://go.dev/) 1.25+
- [Node.js](https://nodejs.org/)
- [Wails](https://wails.io/) v3
- [Task](https://taskfile.dev/)
## Development Workflow (TDD)
```bash
task test-gen # 1. Generate test stubs
task test # 2. Run tests (watch them fail)
# 3. Implement your feature
task test # 4. Run tests (watch them pass)
task review # 5. CodeRabbit review
```
## Building & Running
```bash
# GUI (Wails)
task gui:dev # Development with hot-reload
task gui:build # Production build
# CLI
task cli:build # Build to cmd/core/bin/core
task cli:run # Build and run
```
## All Tasks
| Task | Description |
|------|-------------|
| `task test` | Run all Go tests |
| `task test-gen` | Generate test stubs for public API |
| `task check` | go mod tidy + tests + review |
| `task review` | CodeRabbit review |
| `task cov` | Generate coverage.txt |
| `task cov-view` | Open HTML coverage report |
| `task sync` | Update public API Go files |
---
## Architecture
### Project Structure
```
.
├── core.go # Facade re-exporting pkg/core
├── pkg/
│ ├── core/ # Service container, DI, Runtime[T]
│ ├── config/ # JSON persistence, XDG paths
│ ├── display/ # Windows, tray, menus (Wails)
│ ├── crypt/ # Hashing, checksums, PGP
│ │ └── openpgp/ # Full PGP implementation
│ ├── io/ # Medium interface + backends
│ ├── workspace/ # Encrypted workspace management
│ ├── help/ # In-app documentation
│ └── i18n/ # Internationalization
├── cmd/
│ ├── core/ # CLI application
│ └── core-gui/ # Wails GUI application
└── go.work # Links root, cmd/core, cmd/core-gui
```
### Service Pattern (Dual-Constructor DI)
Every service follows this pattern:
```go
// Static DI - standalone use/testing (no core.Runtime)
func New() (*Service, error)
// Dynamic DI - for core.WithService() registration
func Register(c *core.Core) (any, error)
```
Services embed `*core.Runtime[Options]` for access to `Core()` and `Config()`.
### IPC/Action System
Services implement `HandleIPCEvents(c *core.Core, msg core.Message) error` - auto-discovered via reflection. Handles typed actions like `core.ActionServiceStartup`.
---
## Wails v3 Frontend Bindings
Core uses [Wails v3](https://v3alpha.wails.io/) to expose Go methods to a WebView2 browser runtime. Wails automatically generates TypeScript bindings for registered services.
**Documentation:** [Wails v3 Method Bindings](https://v3alpha.wails.io/features/bindings/methods/)
### How It Works
1. **Go services** with exported methods are registered with Wails
2. Run `wails3 generate bindings` (or `wails3 dev` / `wails3 build`)
3. **TypeScript SDK** is generated in `frontend/bindings/`
4. Frontend calls Go methods with full type safety, no HTTP overhead
### Current Binding Architecture
```go
// cmd/core-gui/main.go
app.RegisterService(application.NewService(coreService)) // Only Core is registered
```
**Problem:** Only `Core` is registered with Wails. Sub-services (crypt, workspace, display, etc.) are internal to Core's service map - their methods aren't directly exposed to JS.
**Currently exposed** (see `cmd/core-gui/public/bindings/`):
```typescript
// From frontend:
import { ACTION, Config, Service } from './bindings/github.com/host-uk/core/pkg/core'
ACTION(msg) // Broadcast IPC message
Config() // Get config service reference
Service("workspace") // Get service by name (returns any)
```
**NOT exposed:** Direct calls like `workspace.CreateWorkspace()` or `crypt.Hash()`.
### The IPC Bridge Pattern (Chosen Architecture)
Sub-services are accessed via Core's **IPC/ACTION system**, not direct Wails bindings:
```typescript
// Frontend calls Core.ACTION() with typed messages
import { ACTION } from './bindings/github.com/host-uk/core/pkg/core'
// Open a window
ACTION({ action: "display.open_window", name: "settings", options: { Title: "Settings", Width: 800 } })
// Switch workspace
ACTION({ action: "workspace.switch_workspace", name: "myworkspace" })
```
Each service implements `HandleIPCEvents(c *core.Core, msg core.Message)` to process these messages:
```go
// pkg/display/display.go
func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) error {
switch m := msg.(type) {
case map[string]any:
if action, ok := m["action"].(string); ok && action == "display.open_window" {
return s.handleOpenWindowAction(m)
}
}
return nil
}
```
**Why this pattern:**
- Single Wails service (Core) = simpler binding generation
- Services remain decoupled from Wails
- Centralized message routing via `ACTION()`
- Services can communicate internally using same pattern
**Current gap:** Not all service methods have IPC handlers yet. See `HandleIPCEvents` in each service to understand what's wired up.
### Generating Bindings
```bash
cd cmd/core-gui
wails3 generate bindings # Regenerate after Go changes
```
Bindings output to `cmd/core-gui/public/bindings/github.com/host-uk/core/` mirroring Go package structure.
---
### Service Interfaces (`pkg/core/interfaces.go`)
```go
type Config interface {
Get(key string, out any) error
Set(key string, v any) error
}
type Display interface {
OpenWindow(opts ...WindowOption) error
}
type Workspace interface {
CreateWorkspace(identifier, password string) (string, error)
SwitchWorkspace(name string) error
WorkspaceFileGet(filename string) (string, error)
WorkspaceFileSet(filename, content string) error
}
type Crypt interface {
EncryptPGP(writer io.Writer, recipientPath, data string, ...) (string, error)
DecryptPGP(recipientPath, message, passphrase string, ...) (string, error)
}
```
---
## Current State (Prototype)
### Working
| Package | Notes |
|---------|-------|
| `pkg/core` | Service container, DI, thread-safe - solid |
| `pkg/config` | JSON persistence, XDG paths - solid |
| `pkg/crypt` | Hashing, checksums, PGP - solid, well-tested |
| `pkg/help` | Embedded docs, Show/ShowAt - solid |
| `pkg/i18n` | Multi-language with go-i18n - solid |
| `pkg/io` | Medium interface + local backend - solid |
| `pkg/workspace` | Workspace creation, switching, file ops - functional |
### Partial
| Package | Issues |
|---------|--------|
| `pkg/display` | Window creation works; menu/tray handlers are TODOs |
---
## Priority Work Items
### 1. IMPLEMENT: System Tray Brand Support
`pkg/display/tray.go:52-63` - Commented brand-specific menu items need implementation.
### 2. ADD: Integration Tests
| Package | Notes |
|---------|-------|
| `pkg/display` | Integration tests requiring Wails runtime (27% unit coverage) |
---
## Package Deep Dives
### pkg/workspace - The Core Feature
Each workspace is:
1. Identified by LTHN hash of user identifier
2. Has directory structure: `config/`, `log/`, `data/`, `files/`, `keys/`
3. Gets a PGP keypair generated on creation
4. Files accessed via obfuscated paths
The `workspaceList` maps workspace IDs to public keys.
### pkg/crypt/openpgp
Full PGP using `github.com/ProtonMail/go-crypto`:
- `CreateKeyPair(name, passphrase)` - RSA-4096 with revocation cert
- `EncryptPGP()` - Encrypt + optional signing
- `DecryptPGP()` - Decrypt + optional signature verification
### pkg/io - Storage Abstraction
```go
type Medium interface {
Read(path string) (string, error)
Write(path, content string) error
EnsureDir(path string) error
IsFile(path string) bool
FileGet(path string) (string, error)
FileSet(path, content string) error
}
```
Implementations: `local/`, `sftp/`, `webdav/`
---
## Future Work
### Phase 1: Core Stability
- [x] ~~Fix workspace medium injection (critical blocker)~~
- [x] ~~Initialize `io.Local` global~~
- [x] ~~Clean up dead code (orphaned vars, broken wrappers)~~
- [x] ~~Wire up IPC handlers for all services (config, crypt, display, help, i18n, workspace)~~
- [x] ~~Complete display menu handlers (New/List workspace)~~
- [x] ~~Tray icon setup with asset embedding~~
- [x] ~~Test coverage for io packages~~
- [ ] System tray brand-specific menus
### Phase 2: Multi-Brand Support
- [ ] Define brand configuration system (config? build flags?)
- [ ] Implement brand-specific tray menus (AdminHub, ServerHub, GatewayHub, DeveloperHub, ClientHub)
- [ ] Brand-specific theming/assets
- [ ] Per-brand default workspace configurations
### Phase 3: Remote Storage
- [ ] Complete SFTP backend (`pkg/io/sftp/`)
- [ ] Complete WebDAV backend (`pkg/io/webdav/`)
- [ ] Workspace sync across storage backends
- [ ] Conflict resolution for multi-device access
### Phase 4: Enhanced Crypto
- [ ] Key management UI (import/export, key rotation)
- [ ] Multi-recipient encryption
- [ ] Hardware key support (YubiKey, etc.)
- [ ] Encrypted workspace backup/restore
### Phase 5: Developer Experience
- [ ] TypeScript types for IPC messages (codegen from Go structs)
- [ ] Hot-reload for service registration
- [ ] Plugin system for third-party services
- [ ] CLI tooling for workspace management
### Phase 6: Distribution
- [ ] Auto-update mechanism
- [ ] Platform installers (DMG, MSI, AppImage)
- [ ] Signing and notarization
- [ ] Crash reporting integration
---
## For New Contributors
1. Run `task test` to verify all tests pass
2. Follow TDD: `task test-gen` creates stubs, implement to pass
3. The dual-constructor pattern is intentional: `New(deps)` for tests, `Register()` for runtime
4. See `cmd/core-gui/main.go` for how services wire together
5. IPC handlers in each service's `HandleIPCEvents()` are the frontend bridge