Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
2902901
docs: refine spec-driven agent workflow
ampagent Jul 31, 2026
8dcc5d9
Merge pull request #253 from techulus/docs/spec-workflow-agent-guidance
arjunkomath Jul 31, 2026
a876cd3
docs: document safe registry garbage collection
arjunkomath Jul 31, 2026
66ebe23
fix command search input font size
ampagent Jul 31, 2026
54723dd
Merge pull request #255 from techulus/docs/safe-registry-garbage-coll…
arjunkomath Jul 31, 2026
d0a4b7d
Merge pull request #256 from techulus/fix/command-search-mobile-font-…
arjunkomath Jul 31, 2026
6369024
Clean up registry images
ampagent Aug 1, 2026
cd4f7fc
Document registry retention configuration
ampagent Aug 1, 2026
e17b41d
Merge pull request #257 from techulus/feat/registry-image-retention
arjunkomath Aug 1, 2026
81e597e
feat: add global registry authentication
ampagent Aug 1, 2026
af3ade0
refactor: make registry credentials immutable
ampagent Aug 1, 2026
b383d7b
chore: simplify registry settings copy
ampagent Aug 1, 2026
607055a
Merge pull request #258 from techulus/feature/custom-registry-auth
arjunkomath Aug 1, 2026
111f931
Reduce dashboard card height
arjunkomath Aug 1, 2026
881865d
Merge pull request #259 from techulus/ui/dashboard-card-height
arjunkomath Aug 1, 2026
fdc63a8
docs: combine specification and planning phases
arjunkomath Aug 1, 2026
a647be5
docs: clarify spec-driven tool workflow
arjunkomath Aug 1, 2026
ad08564
Merge pull request #260 from techulus/docs/combine-spec-planning
arjunkomath Aug 1, 2026
9c86e31
Fix restore completion status wedge
techulus-agent Aug 1, 2026
0e583ae
Merge pull request #261 from techulus/fix/missing-backup-status-wedge
arjunkomath Aug 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 47 additions & 27 deletions AGENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,61 +39,81 @@ An open container deployment platform. See README.md for architecture.

## Spec-driven development workflow

For any requested code or configuration change, work through these phases in
order. Do not collapse requirements, specification, and implementation
planning into a single step.
For any requested code or configuration change, follow this order: research
and requirements confirmation, combined specification/development planning,
explicit approval of the completed plan, then implementation. Do not collapse
requirements refinement and specification/planning. In each phase, use its
named tool when available; otherwise follow that phase's fallback.

Research notes, requirements, specifications, and plans are workflow artifacts
and may be written or updated before approval. Do not change product code or
configuration until the user explicitly approves the completed specification
and development plan.

- Use subagents where helpful for bounded research, investigation, independent
analysis, and synthesizing findings or answers.
- As each stage is completed, compact the working context into its agreed
deliverable before progressing. Preserve material decisions, constraints,
assumptions, unresolved questions, and risks.

### 1. Research and refine requirements

Understand the problem before designing a solution.

- Inspect the relevant code, documentation, existing behavior, and project
constraints.
- Clarify the desired outcome, scope, non-goals, edge cases, and acceptance
criteria.
- Identify assumptions and ask focused questions when ambiguity would
materially affect the solution.
- Present the refined requirements for confirmation.
Use `research_codebase` when available: make its readiness call, send the
research question, and use same-session follow-ups for further investigation.
Synthesize its findings into requirements, constraints, assumptions,
non-goals, edge cases, and acceptance criteria; resolve material ambiguities
and present the refined requirements for confirmation.

If `research_codebase` is unavailable, inspect the relevant code and
constraints directly, summarize the same requirements, and obtain user
confirmation.

Deliverable: agreed requirements, constraints, assumptions, and acceptance
criteria.

### 2. Build the specification
### 2. Build the specification and development plan

Describe what will be built and how it should work.
Begin only after requirements are confirmed. Use `create_plan` when available:
start with the confirmed requirements and relevant research, then use
same-session follow-ups to resolve decisions and incorporate feedback. An
outline approval permits detailed-plan development only; implementation
requires explicit approval of the completed plan.

- Define user-visible and system behavior.
- Describe the technical approach, architecture, interfaces, data flow, and
error handling.
- Address important edge cases and consequential tradeoffs.
- Keep the specification solution-level rather than file-by-file.

Deliverable: a reviewable specification of the intended behavior and technical
design.

### 3. Create the development plan

Translate the specification into concrete implementation work.

- List the files and modules that will be added, changed, renamed, or removed.
- Describe the specific changes required in each location.
- Include API, schema, type, dependency, and configuration changes where
applicable.
- Define the tests and verification commands that will be run.
- Order the work into small, reviewable steps and identify remaining risks.

Deliverable: an actionable, file-level development plan.
If `create_plan` is unavailable, define the behavior, architecture, edge cases,
file changes, and verification directly, resolve consequential decisions, and
present the complete plan for explicit approval.

Deliverable: a reviewable specification of the intended behavior and technical
design, plus an actionable, file-level development plan.

### 3. Implement after approval

### 4. Implement after approval
After explicit approval, use `implement_plan` when available, starting with the
approved plan path. By default, complete one approved phase, run its automated
verification, update plan checkboxes, report the manual verification steps,
and pause for explicit confirmation before continuing.

Do not modify the codebase until the user approves the development plan.
If `implement_plan` is unavailable, follow the same phase-by-phase process and
stop on any material mismatch.

- Implement the approved plan using the smallest correct changes and existing
project patterns.
- Run the planned verification and report the results honestly.
- If new information requires a material change to the requirements,
specification, scope, or architecture, pause implementation and return to
the appropriate phase for approval.
- If a material mismatch affects requirements, specification, scope, or
architecture, stop and return to the appropriate phase for approval.
- Resolve minor implementation details autonomously when they do not alter the
approved behavior or scope.

Expand Down
56 changes: 25 additions & 31 deletions agent/cmd/agent/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"techulus/cloud-agent/internal/network"
"techulus/cloud-agent/internal/paths"
"techulus/cloud-agent/internal/reconcile"
"techulus/cloud-agent/internal/registryauth"
"techulus/cloud-agent/internal/routeowners"
"techulus/cloud-agent/internal/traefik"
"techulus/cloud-agent/internal/wireguard"
Expand Down Expand Up @@ -126,6 +127,9 @@ func main() {
if err != nil {
log.Fatalf("Failed to load config: %v", err)
}
if err := configuration.Save(config); err != nil {
log.Fatalf("Failed to rewrite config: %v", err)
}

log.Printf("Loaded config: serverID=%s, subnetId=%d, wireguardIP=%s", config.ServerID, config.SubnetID, config.WireGuardIP)

Expand Down Expand Up @@ -194,29 +198,14 @@ func main() {
respMetricsEndpoint = *resp.MetricsEndpoint
}

var registryURL, registryUsername, registryPassword string
if resp.RegistryURL != nil {
registryURL = *resp.RegistryURL
}
if resp.RegistryUsername != nil {
registryUsername = *resp.RegistryUsername
}
if resp.RegistryPassword != nil {
registryPassword = *resp.RegistryPassword
}

config = &agent.Config{
ServerID: resp.ServerID,
SubnetID: resp.SubnetID,
WireGuardIP: resp.WireGuardIP,
EncryptionKey: resp.EncryptionKey,
IsProxy: isProxy,
LoggingEndpoint: respLoggingEndpoint,
MetricsEndpoint: respMetricsEndpoint,
RegistryURL: registryURL,
RegistryUsername: registryUsername,
RegistryPassword: registryPassword,
RegistryInsecure: resp.RegistryInsecure,
ServerID: resp.ServerID,
SubnetID: resp.SubnetID,
WireGuardIP: resp.WireGuardIP,
EncryptionKey: resp.EncryptionKey,
IsProxy: isProxy,
LoggingEndpoint: respLoggingEndpoint,
MetricsEndpoint: respMetricsEndpoint,
}

if logsEndpointFlag != "" {
Expand Down Expand Up @@ -275,15 +264,20 @@ func main() {
}
}

if config.RegistryURL != "" && config.RegistryUsername != "" {
log.Printf("[registry] attempting login to %s", config.RegistryURL)
if err := container.Login(config.RegistryURL, config.RegistryUsername, config.RegistryPassword, config.RegistryInsecure); err != nil {
log.Printf("[registry] warning: failed to login to registry: %v", err)
}
}

reconciler := reconcile.NewReconciler(config.EncryptionKey, dataDir, config.RegistryInsecure)
client := agenthttp.NewClient(controlPlaneURL, config.ServerID, signingKeyPair, dataDir)
registryManager, err := registryauth.NewManager(dataDir, config.EncryptionKey, client)
if err != nil {
log.Fatalf("Failed to initialize registry authentication: %v", err)
}
if err := registryManager.MarkDirty("startup"); err != nil {
log.Fatalf("Failed to require initial registry synchronization: %v", err)
}
initialCtx, initialCancel := context.WithTimeout(context.Background(), 30*time.Second)
if err := registryManager.Sync(initialCtx); err != nil {
log.Printf("[registry] initial sync failed: %v", err)
}
initialCancel()
reconciler := reconcile.NewReconciler(config.EncryptionKey, dataDir, registryManager)

var logCollector *logs.Collector
var traefikLogCollector *logs.TraefikCollector
Expand Down Expand Up @@ -339,7 +333,7 @@ func main() {
privateIP := network.PrivateIP()
log.Printf("Agent %s started. Public IP: %s, Private IP: %s. Tick interval: %v", agent.Version, publicIP, privateIP, agent.TickInterval)

agentInstance := agent.NewAgent(client, reconciler, config, publicIP, privateIP, dataDir, logCollector, traefikLogCollector, metricsSender, routeOwners, builder, config.IsProxy, disableDNS)
agentInstance := agent.NewAgent(client, reconciler, config, publicIP, privateIP, dataDir, logCollector, traefikLogCollector, metricsSender, routeOwners, builder, registryManager, config.IsProxy, disableDNS)
agentInstance.Run(ctx)

if agentLogFlusherDone != nil {
Expand Down
22 changes: 11 additions & 11 deletions agent/internal/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
agenthttp "techulus/cloud-agent/internal/http"
"techulus/cloud-agent/internal/logs"
"techulus/cloud-agent/internal/reconcile"
"techulus/cloud-agent/internal/registryauth"
"techulus/cloud-agent/internal/routeowners"
)

Expand All @@ -28,17 +29,13 @@ const (
)

type Config struct {
ServerID string `json:"serverId"`
SubnetID int `json:"subnetId"`
WireGuardIP string `json:"wireguardIp"`
EncryptionKey string `json:"encryptionKey"`
IsProxy bool `json:"isProxy"`
LoggingEndpoint string `json:"loggingEndpoint,omitempty"`
MetricsEndpoint string `json:"metricsEndpoint,omitempty"`
RegistryURL string `json:"registryUrl,omitempty"`
RegistryUsername string `json:"registryUsername,omitempty"`
RegistryPassword string `json:"registryPassword,omitempty"`
RegistryInsecure bool `json:"registryInsecure"`
ServerID string `json:"serverId"`
SubnetID int `json:"subnetId"`
WireGuardIP string `json:"wireguardIp"`
EncryptionKey string `json:"encryptionKey"`
IsProxy bool `json:"isProxy"`
LoggingEndpoint string `json:"loggingEndpoint,omitempty"`
MetricsEndpoint string `json:"metricsEndpoint,omitempty"`
}

type ActualState struct {
Expand Down Expand Up @@ -89,6 +86,7 @@ type Agent struct {
MetricsSender MetricsSender
RouteOwners *routeowners.Registry
Builder *build.Builder
RegistryAuth *registryauth.Manager
isBuilding bool
buildMutex sync.Mutex
currentBuildID string
Expand All @@ -107,6 +105,7 @@ func NewAgent(
metricsSender MetricsSender,
routeOwners *routeowners.Registry,
builder *build.Builder,
registryAuth *registryauth.Manager,
isProxy bool,
disableDNS bool,
) *Agent {
Expand All @@ -126,6 +125,7 @@ func NewAgent(
MetricsSender: metricsSender,
RouteOwners: routeOwners,
Builder: builder,
RegistryAuth: registryAuth,
IsProxy: isProxy,
DisableDNS: disableDNS,
deploymentDeployLocks: map[string]*sync.Mutex{},
Expand Down
18 changes: 2 additions & 16 deletions agent/internal/agent/drift.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import (
"context"
"fmt"
"log"
"strings"
"time"

"techulus/cloud-agent/internal/container"
"techulus/cloud-agent/internal/dns"
agenthttp "techulus/cloud-agent/internal/http"
"techulus/cloud-agent/internal/registryauth"
"techulus/cloud-agent/internal/retry"
"techulus/cloud-agent/internal/traefik"
"techulus/cloud-agent/internal/wireguard"
Expand Down Expand Up @@ -403,21 +403,7 @@ func (a *Agent) planReconcile(expected *agenthttp.ExpectedState, actual *ActualS
}

func normalizeImage(image string) string {
digest := ""
if digestIndex := strings.Index(image, "@"); digestIndex != -1 {
digest = image[digestIndex:]
image = image[:digestIndex]
}

image = strings.TrimPrefix(image, "docker.io/library/")
image = strings.TrimPrefix(image, "docker.io/")

lastSlash := strings.LastIndex(image, "/")
lastColon := strings.LastIndex(image, ":")
if digest == "" && lastColon <= lastSlash {
image = image + ":latest"
}
return image + digest
return registryauth.NormalizeImage(image)
}

func desiredContainerState(container agenthttp.ExpectedContainer) string {
Expand Down
Loading
Loading