Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,13 @@ agent_evidence:

See [configuration](./docs/configuration.md) for more information.

The agent adds `_agent_config_hash` to plugin and agent evidence labels. The value is a deterministic SHA-256 hash of
the runtime plugin and agent evidence configuration, which prevents multiple unauthenticated agents using the fallback
`_agent=ccf` identity from writing to the same evidence seed when their configurations differ.

Comment thread
gusfcarvalho marked this conversation as resolved.
### Environment variables

The agent can load specific configruation values from environment variables, which are prefixed with `CCF_` and the path
The agent can load specific configuration values from environment variables, which are prefixed with `CCF_` and the path
Comment thread
gusfcarvalho marked this conversation as resolved.
in the config is specified using underscore-separated key.

For example, to specify the `token` value in the GitHub config, you may set an environment variable `CCF_PLUGINS_GITHUB_CONFIG_TOKEN`
Expand Down
160 changes: 138 additions & 22 deletions cmd/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cmd
import (
"bytes"
"context"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
Expand Down Expand Up @@ -199,6 +200,7 @@ const RunnerV2ProtocolVersion int32 = 2
const AnnotationProtocolVersionKey = "org.ccf.plugin.protocol.version"
const daemonCronStopTimeout = 30 * time.Second
const agentEvidenceErrorArtifactMaxBytes = 1024 * 1024
const agentConfigHashLabel = "_agent_config_hash"

type pluginRunStatus string

Expand Down Expand Up @@ -839,12 +841,141 @@ func agentIdentityLabel(config *agentConfig) string {

func agentFoundationalLabels(config *agentConfig) map[string]string {
return map[string]string{
"_agent": agentIdentityLabel(config),
"tool": "ccf",
"type": "operations",
"_agent": agentIdentityLabel(config),
agentConfigHashLabel: agentConfigurationHash(config),
"tool": "ccf",
"type": "operations",
}
}

type normalizedAgentConfigForHash struct {
AgentEvidence normalizedAgentEvidenceConfigForHash `json:"agent_evidence"`
Plugins []normalizedAgentPluginForHash `json:"plugins"`
}

type normalizedAgentEvidenceConfigForHash struct {
Enabled bool `json:"enabled"`
EmitOnRunCompletion bool `json:"emit_on_run_completion"`
Interval string `json:"interval"`
}

type normalizedAgentPluginForHash struct {
Name string `json:"name"`
ProtocolVersion int32 `json:"protocol_version"`
Schedule string `json:"schedule"`
Source string `json:"source"`
Policies []string `json:"policies"`
Config map[string]string `json:"config,omitempty"`
Labels map[string]string `json:"labels,omitempty"`
}

func agentConfigurationHash(config *agentConfig) string {
normalized := normalizedAgentConfigForHash{
AgentEvidence: normalizedAgentEvidenceConfigForHash{
Enabled: true,
EmitOnRunCompletion: true,
Interval: normalizedAgentEvidenceInterval(config),
},
}
Comment thread
gusfcarvalho marked this conversation as resolved.

if config != nil {
normalized.AgentEvidence.Enabled = config.agentEvidenceEnabled()
normalized.AgentEvidence.EmitOnRunCompletion = config.agentEvidenceEmitOnRunCompletion()

pluginNames := make([]string, 0, len(config.Plugins))
for pluginName := range config.Plugins {
pluginNames = append(pluginNames, pluginName)
}
sort.Strings(pluginNames)

normalized.Plugins = make([]normalizedAgentPluginForHash, 0, len(pluginNames))
for _, pluginName := range pluginNames {
pluginConfig := config.Plugins[pluginName]
normalizedPlugin := normalizedAgentPluginForHash{
Name: pluginName,
Schedule: "* * * * *",
}
Comment thread
gusfcarvalho marked this conversation as resolved.
if pluginConfig != nil {
normalizedPlugin.ProtocolVersion = effectivePluginProtocolVersion(pluginConfig)
normalizedPlugin.Source = pluginConfig.Source
if pluginConfig.Schedule != nil {
normalizedPlugin.Schedule = *pluginConfig.Schedule
}
normalizedPlugin.Policies = make([]string, 0, len(pluginConfig.Policies))
for _, policy := range pluginConfig.Policies {
normalizedPlugin.Policies = append(normalizedPlugin.Policies, string(policy))
}
normalizedPlugin.Config = copyStringMap(pluginConfig.Config)
Comment thread
gusfcarvalho marked this conversation as resolved.
normalizedPlugin.Labels = copyStringMap(pluginConfig.Labels)
}
normalized.Plugins = append(normalized.Plugins, normalizedPlugin)
}
}

payload, err := json.Marshal(normalized)
if err != nil {
sum := sha256.Sum256([]byte(err.Error()))
return fmt.Sprintf("%x", sum[:])
}
Comment thread
gusfcarvalho marked this conversation as resolved.
sum := sha256.Sum256(payload)
return fmt.Sprintf("%x", sum[:])
}

func normalizedAgentEvidenceInterval(config *agentConfig) string {
if config == nil {
return time.Hour.String()
}

interval, err := config.agentEvidenceInterval()
if err != nil {
if config.AgentEvidence == nil {
return ""
}
return strings.TrimSpace(config.AgentEvidence.Interval)
}
return interval.String()
}

func copyStringMap(input map[string]string) map[string]string {
if len(input) == 0 {
return nil
}

output := make(map[string]string, len(input))
for key, value := range input {
output[key] = value
}
return output
}

Comment thread
gusfcarvalho marked this conversation as resolved.
Comment thread
gusfcarvalho marked this conversation as resolved.
func pluginEvidenceLabels(config *agentConfig, pluginName string, pluginConfig *agentPlugin) map[string]string {
return pluginEvidenceLabelsWithHash(config, pluginName, pluginConfig, agentConfigurationHash(config))
}

func pluginEvidenceLabelsWithHash(config *agentConfig, pluginName string, pluginConfig *agentPlugin, configHash string) map[string]string {
labels := map[string]string{
"_agent": agentIdentityLabel(config),
"_plugin": pluginName,
}
if pluginConfig != nil {
for k, v := range pluginConfig.Labels {
Comment thread
gusfcarvalho marked this conversation as resolved.
labels[k] = v
}
}
Comment thread
gusfcarvalho marked this conversation as resolved.
labels[agentConfigHashLabel] = configHash
return labels
Comment thread
gusfcarvalho marked this conversation as resolved.
}

func effectivePluginProtocolVersion(pluginConfig *agentPlugin) int32 {
if pluginConfig == nil {
return 0
}
if pluginConfig.ProtocolVersion == 0 && !pluginConfig.protocolSet {
return DefaultProtocolVersion
}
return pluginConfig.ProtocolVersion
}

func maskClientID(clientID string) string {
trimmed := strings.TrimSpace(clientID)
if trimmed == "" {
Expand Down Expand Up @@ -1186,6 +1317,7 @@ func (ar *AgentRunner) runAllPlugins(ctx context.Context) error {
pluginNames = append(pluginNames, pluginName)
}
sort.Strings(pluginNames)
configHash := agentConfigurationHash(config)

for _, pluginName := range pluginNames {
pluginConfig := config.Plugins[pluginName]
Expand All @@ -1196,13 +1328,7 @@ func (ar *AgentRunner) runAllPlugins(ctx context.Context) error {
Level: hclog.Level(config.logVerbosity()),
})

labels := map[string]string{
"_agent": agentIdentityLabel(config),
"_plugin": pluginName,
}
for k, v := range pluginConfig.Labels {
labels[k] = v
}
labels := pluginEvidenceLabelsWithHash(config, pluginName, pluginConfig, configHash)

source := ar.pluginLocations[pluginConfig.Source]

Expand Down Expand Up @@ -1362,13 +1488,7 @@ func (ar *AgentRunner) runPlugin(ctx context.Context, name string, plugin *agent
Level: hclog.Level(config.logVerbosity()),
})

labels := map[string]string{
"_agent": agentIdentityLabel(config),
"_plugin": name,
}
for k, v := range plugin.Labels {
labels[k] = v
}
labels := pluginEvidenceLabelsWithHash(config, name, plugin, agentConfigurationHash(config))

pluginLogger.Debug("Running plugin", "source", pluginExecutable, "protocol_version", plugin.ProtocolVersion)

Expand Down Expand Up @@ -1516,11 +1636,7 @@ func (ar *AgentRunner) buildAgentRunEvidence(now time.Time) (*agentEvidenceCreat
description := formatAgentEvidenceDescription(snapshot)
remarks := formatAgentEvidenceRemarks(snapshot)
labels := agentFoundationalLabels(config)
evidenceUUID, err := sdk.SeededUUID(map[string]string{
"type": labels["type"],
"_agent": labels["_agent"],
"tool": labels["tool"],
})
evidenceUUID, err := sdk.SeededUUID(labels)
if err != nil {
return nil, err
}
Expand Down
Loading
Loading