Skip to content

Commit 72b5af5

Browse files
committed
Merge remote-tracking branch 'origin/main' into feat/control-plane-mcp-server
# Conflicts: # control-plane/internal/server/server.go
2 parents 0bb7e7d + 4a64cde commit 72b5af5

64 files changed

Lines changed: 4719 additions & 128 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,118 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
66

77
<!-- changelog:entries -->
88

9+
## [0.1.117-rc.3] - 2026-07-24
10+
11+
12+
### Other
13+
14+
- Add tests for agentic handlers: query, batch, discover (#810)
15+
16+
* issue/agentic-handler-tests: add tests for query, batch, and discover handlers
17+
18+
Adds three new test files covering edge cases and missing code paths:
19+
- query_handler_test.go: default limit clamping, invalid RFC3339 dates,
20+
offset out-of-bounds, missing resource field, response structure
21+
- batch_test.go: invalid JSON, single/max operations, POST bodies,
22+
sub-request errors, auth header propagation, concurrent integrity
23+
- discover_test.go: method filter, limit clamp >100, combined filters,
24+
see_also references, Smart404 suggestions with auth filtering
25+
26+
Coverage: 91.5% -> 92.6% (BatchHandler: 79.4% -> 94.1%,
27+
DiscoverHandler: 92.3% -> 100.0%)
28+
29+
* fix(agentic): preserve batch caller identity
30+
31+
* chore(skills): sync embedded skill mirrors on branch
32+
33+
Realigns embedded mirrors with skills/ sources inherited from the main
34+
merge so skillkit drift tests pass branch-locally.
35+
36+
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
37+
38+
* test(agentic): cover past-end offset branches and node-ID batch forwarding
39+
40+
Closes the patch-coverage gap on the caller-identity fix: the empty-page
41+
branches for executions/workflows/sessions (only agents was exercised)
42+
and the X-Agent-Node-ID forwarding branch in batch sub-requests.
43+
44+
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
45+
46+
---------
47+
48+
Co-authored-by: Claude Fable 5 <noreply@anthropic.com> (fde785d)
49+
50+
## [0.1.117-rc.2] - 2026-07-24
51+
52+
53+
### Other
54+
55+
- Register pprof debug endpoints behind admin-token auth (#809)
56+
57+
* issue/pprof-admin-endpoints: register pprof debug endpoints behind admin-token auth
58+
59+
Add /debug/pprof/ routes gated by X-Admin-Token (returns 401 on missing/wrong
60+
token, 200 with valid token). Named profiles (goroutine, heap, etc.) share the
61+
same gating. When no admin token is configured the endpoints are open,
62+
consistent with existing AdminTokenAuth behavior.
63+
64+
* fix(server): complete pprof admin endpoint handling
65+
66+
* chore(skills): sync embedded skill mirrors on branch
67+
68+
Realigns embedded mirrors with skills/ sources inherited from the main
69+
merge so skillkit drift tests pass branch-locally.
70+
71+
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
72+
73+
---------
74+
75+
Co-authored-by: Claude Fable 5 <noreply@anthropic.com> (a2e13d0)
76+
77+
## [0.1.117-rc.1] - 2026-07-24
78+
79+
80+
### Documentation
81+
82+
- Docs(skills): agentfield-use — default to full batch dispatch; canary-validate after node reconfig (#827)
83+
84+
Two additions to the concurrency guidance:
85+
86+
- Make the batch default explicit: when N independent jobs arrive, dispatch
87+
all of them up front and poll as a group, rather than one-at-a-time.
88+
89+
- Add the one exception: right after a node's runtime config changes
90+
(provider/model/bin path via af secrets set + restart), send ONE
91+
representative call and verify it did real work (nonzero cost/duration,
92+
plausible output) before fanning out. A misconfigured harness can crash
93+
instantly while the run still reports succeeded-with-empty-results, and
94+
an agent that posts externally (e.g. GitHub reviews) publishes that
95+
garbage under the user's identity once per dispatched call.
96+
97+
Co-authored-by: Claude Fable 5 <noreply@anthropic.com> (613138b)
98+
99+
100+
101+
### Other
102+
103+
- Issue/presence-start-guard: guard PresenceManager.Start against duplicate sweep goroutines (#808)
104+
105+
Start() now uses sync.Once (mirroring the existing stopOnce pattern) to
106+
ensure only one sweep loop is ever spawned per PresenceManager instance.
107+
108+
Adds two regression tests:
109+
- TestPresenceManager_Start_Idempotent: counts goroutines before/after
110+
multiple Start() calls to assert no duplicate sweep loops spawn.
111+
- TestPresenceManager_ExpireCallback_OncePerExpiration: counts callback
112+
invocations across multiple sweep cycles to assert the callback fires
113+
at most once per expired node lease.
114+
115+
Verification: temporarily reverted the startOnce guard and confirmed
116+
TestPresenceManager_Start_Idempotent fails with delta=3 (3 extra
117+
goroutines spawned by duplicate Start() calls), proving the regression
118+
test catches the bug. Restored the guard; all presence manager tests
119+
pass and go vet is clean. (4aaf3b0)
120+
9121
## [0.1.116] - 2026-07-24
10122

11123
## [0.1.116-rc.1] - 2026-07-24

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.1.116
1+
0.1.117-rc.3

control-plane/.env.example

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ AGENTFIELD_UI_DEV_PORT=5173
2020
# API Configuration
2121
AGENTFIELD_API_CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173,http://localhost:8080
2222
AGENTFIELD_API_CORS_ALLOWED_METHODS=GET,POST,PUT,DELETE,OPTIONS
23-
AGENTFIELD_API_CORS_ALLOWED_HEADERS=Origin,Content-Type,Accept,Authorization,X-Requested-With
23+
AGENTFIELD_API_CORS_ALLOWED_HEADERS=Origin,Content-Type,Accept,Authorization,X-Requested-With,X-API-Key,X-Admin-Token
2424
AGENTFIELD_API_CORS_EXPOSED_HEADERS=Content-Length,X-Total-Count
2525
AGENTFIELD_API_CORS_ALLOW_CREDENTIALS=true
2626

@@ -49,7 +49,8 @@ AGENTFIELD_STORAGE_MODE=local
4949
# AGENTFIELD_STORAGE_CLOUD_REPLICATION_MODE=async
5050

5151
# VC Authorization
52-
# Admin token for admin API endpoints (tag approval, policy management)
52+
# Admin token for admin API endpoints (tag approval, policy management) and
53+
# /debug/pprof/* endpoints. Send it with the X-Admin-Token header.
5354
# AGENTFIELD_AUTHORIZATION_ADMIN_TOKEN=admin-secret
5455
# Internal token sent to agents during request forwarding.
5556
# Agents with RequireOriginAuth=true validate this to ensure only the control plane can invoke them.

control-plane/internal/cli/commands/install.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ contain its own agentfield-package.yaml; that subtree becomes the package root.
5757
--path is relative to the source root and may not escape it. It composes with an
5858
@ref pin on a Git URL.
5959
60+
To see what environment variables a node needs BEFORE installing it, run
61+
"af show-requirements <path-or-git-url>".
62+
6063
Examples:
6164
agentfield install ./my-agent
6265
agentfield install https://github.com/user/agent-repo

control-plane/internal/cli/config.go

Lines changed: 78 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"os"
77
"path/filepath"
88
"regexp"
9+
"strconv"
910
"strings"
1011
"syscall"
1112

@@ -75,7 +76,7 @@ func runConfigCommand(cmd *cobra.Command, args []string) {
7576
fmt.Printf("❌ Failed to set variable: %v\n", err)
7677
os.Exit(1)
7778
}
78-
fmt.Printf("✅ Set %s for package %s\n", parts[0], packageName)
79+
fmt.Printf("✅ Saved %s for %s (secret store + .env). `af run %s` will pick it up.\n", parts[0], packageName, packageName)
7980
return
8081
}
8182

@@ -153,12 +154,19 @@ func (pcm *PackageConfigManager) InteractiveConfig(packageName string) error {
153154
fmt.Println()
154155
}
155156

156-
// Save environment file
157+
// Save environment file (read by `af dev` and the web UI env editor).
157158
if err := pcm.saveEnvFile(packagePath, envVars); err != nil {
158159
return fmt.Errorf("failed to save environment file: %w", err)
159160
}
160161

161-
fmt.Printf("✅ Environment configuration saved to: %s/.env\n", packagePath)
162+
// Write-through to the encrypted secret store so `af run` picks these up: it
163+
// resolves a node's environment from the store, not the package .env. Without
164+
// this mirror the .env looks configured but the values never reach the process.
165+
if err := pcm.writeNodeSecrets(packageName, envVars); err != nil {
166+
return err
167+
}
168+
169+
fmt.Printf("✅ Saved configuration for %s (secret store + %s/.env)\n", packageName, packagePath)
162170
fmt.Printf("💡 Run 'af run %s' to start the agent with these settings\n", packageName)
163171

164172
return nil
@@ -333,8 +341,48 @@ func (pcm *PackageConfigManager) SetVariable(packageName, key, value string) err
333341
// Set the variable
334342
envVars[key] = value
335343

336-
// Save environment file
337-
return pcm.saveEnvFile(packagePath, envVars)
344+
// Persist to the package .env (read by `af dev` and the web UI env editor).
345+
if err := pcm.saveEnvFile(packagePath, envVars); err != nil {
346+
return err
347+
}
348+
349+
// Write-through to the node-scoped encrypted secret store so `af run` — which
350+
// resolves a node's environment from the store, not the package .env — picks
351+
// it up. This keeps `af config --set` coherent with
352+
// `af secrets set <KEY> --node <package>`; without it the value looks saved but
353+
// never reaches the running process.
354+
return pcm.setNodeSecret(packageName, key, value)
355+
}
356+
357+
// setNodeSecret stores a single value as a node-scoped secret in the same
358+
// encrypted store `af secrets set <KEY> --node <package>` writes to.
359+
func (pcm *PackageConfigManager) setNodeSecret(packageName, key, value string) error {
360+
store, err := packages.NewSecretStore(pcm.AgentFieldHome)
361+
if err != nil {
362+
return fmt.Errorf("failed to open secret store: %w", err)
363+
}
364+
if err := store.Set(packageName, key, value); err != nil {
365+
return fmt.Errorf("failed to write %s to secret store: %w", key, err)
366+
}
367+
return nil
368+
}
369+
370+
// writeNodeSecrets mirrors a whole configured environment into the node-scoped
371+
// encrypted secret store, skipping empty values.
372+
func (pcm *PackageConfigManager) writeNodeSecrets(packageName string, envVars map[string]string) error {
373+
store, err := packages.NewSecretStore(pcm.AgentFieldHome)
374+
if err != nil {
375+
return fmt.Errorf("failed to open secret store: %w", err)
376+
}
377+
for key, value := range envVars {
378+
if value == "" {
379+
continue
380+
}
381+
if err := store.Set(packageName, key, value); err != nil {
382+
return fmt.Errorf("failed to write %s to secret store: %w", key, err)
383+
}
384+
}
385+
return nil
338386
}
339387

340388
// UnsetVariable removes an environment variable
@@ -354,8 +402,21 @@ func (pcm *PackageConfigManager) UnsetVariable(packageName, key string) error {
354402
// Remove the variable
355403
delete(envVars, key)
356404

357-
// Save environment file
358-
return pcm.saveEnvFile(packagePath, envVars)
405+
// Save environment file.
406+
if err := pcm.saveEnvFile(packagePath, envVars); err != nil {
407+
return err
408+
}
409+
410+
// Mirror the removal into the node-scoped secret store so a value unset here
411+
// does not linger and get re-injected by `af run` (which reads the store).
412+
store, err := packages.NewSecretStore(pcm.AgentFieldHome)
413+
if err != nil {
414+
return fmt.Errorf("failed to open secret store: %w", err)
415+
}
416+
if err := store.Delete(packageName, key); err != nil {
417+
return fmt.Errorf("failed to remove %s from secret store: %w", key, err)
418+
}
419+
return nil
359420
}
360421

361422
// loadPackageMetadata loads package metadata and returns the package path
@@ -415,9 +476,13 @@ func (pcm *PackageConfigManager) loadEnvFile(packagePath string) (map[string]str
415476
key := strings.TrimSpace(parts[0])
416477
value := strings.TrimSpace(parts[1])
417478

418-
// Remove quotes if present
419-
if (strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"")) ||
420-
(strings.HasPrefix(value, "'") && strings.HasSuffix(value, "'")) {
479+
// strconv.Quote is used by saveEnvFile, so unquote double-quoted
480+
// values to preserve escaped quotes, backslashes, and newlines.
481+
if strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"") {
482+
if unquoted, err := strconv.Unquote(value); err == nil {
483+
value = unquoted
484+
}
485+
} else if strings.HasPrefix(value, "'") && strings.HasSuffix(value, "'") {
421486
value = value[1 : len(value)-1]
422487
}
423488

@@ -438,9 +503,10 @@ func (pcm *PackageConfigManager) saveEnvFile(packagePath string, envVars map[str
438503
lines = append(lines, "")
439504

440505
for key, value := range envVars {
441-
// Quote values that contain spaces or special characters
506+
// Use Go string quoting so every value can be parsed back without loss.
507+
// This covers embedded quotes, backslashes, and newlines as well as spaces.
442508
if strings.ContainsAny(value, " \t\n\r\"'\\$") {
443-
value = fmt.Sprintf("\"%s\"", strings.ReplaceAll(value, "\"", "\\\""))
509+
value = strconv.Quote(value)
444510
}
445511
lines = append(lines, fmt.Sprintf("%s=%s", key, value))
446512
}

0 commit comments

Comments
 (0)