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
67 changes: 41 additions & 26 deletions cmd/gait/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,35 +16,40 @@ import (
"github.com/Clyra-AI/gait/core/mcp"
"github.com/Clyra-AI/gait/core/pack"
"github.com/Clyra-AI/gait/core/runpack"
schemacommon "github.com/Clyra-AI/gait/core/schema/v1/common"
schemagate "github.com/Clyra-AI/gait/core/schema/v1/gate"
schemarunpack "github.com/Clyra-AI/gait/core/schema/v1/runpack"
sign "github.com/Clyra-AI/proof/signing"
)

type mcpProxyOutput struct {
OK bool `json:"ok"`
Executed bool `json:"executed"`
Adapter string `json:"adapter,omitempty"`
RunID string `json:"run_id,omitempty"`
JobID string `json:"job_id,omitempty"`
Phase string `json:"phase,omitempty"`
SessionID string `json:"session_id,omitempty"`
ToolName string `json:"tool_name,omitempty"`
Verdict string `json:"verdict,omitempty"`
ReasonCodes []string `json:"reason_codes,omitempty"`
Violations []string `json:"violations,omitempty"`
PolicyDigest string `json:"policy_digest,omitempty"`
IntentDigest string `json:"intent_digest,omitempty"`
DecisionLatencyMS int64 `json:"decision_latency_ms,omitempty"`
TraceID string `json:"trace_id,omitempty"`
TracePath string `json:"trace_path,omitempty"`
RunpackPath string `json:"runpack_path,omitempty"`
PackPath string `json:"pack_path,omitempty"`
PackID string `json:"pack_id,omitempty"`
LogExport string `json:"log_export,omitempty"`
OTelExport string `json:"otel_export,omitempty"`
Warnings []string `json:"warnings,omitempty"`
Error string `json:"error,omitempty"`
OK bool `json:"ok"`
Executed bool `json:"executed"`
Adapter string `json:"adapter,omitempty"`
RunID string `json:"run_id,omitempty"`
JobID string `json:"job_id,omitempty"`
Phase string `json:"phase,omitempty"`
SessionID string `json:"session_id,omitempty"`
ToolName string `json:"tool_name,omitempty"`
Verdict string `json:"verdict,omitempty"`
ReasonCodes []string `json:"reason_codes,omitempty"`
Violations []string `json:"violations,omitempty"`
PolicyDigest string `json:"policy_digest,omitempty"`
PolicyID string `json:"policy_id,omitempty"`
PolicyVersion string `json:"policy_version,omitempty"`
MatchedRuleIDs []string `json:"matched_rule_ids,omitempty"`
IntentDigest string `json:"intent_digest,omitempty"`
DecisionLatencyMS int64 `json:"decision_latency_ms,omitempty"`
TraceID string `json:"trace_id,omitempty"`
TracePath string `json:"trace_path,omitempty"`
RunpackPath string `json:"runpack_path,omitempty"`
PackPath string `json:"pack_path,omitempty"`
PackID string `json:"pack_id,omitempty"`
LogExport string `json:"log_export,omitempty"`
OTelExport string `json:"otel_export,omitempty"`
Warnings []string `json:"warnings,omitempty"`
Relationship *schemacommon.RelationshipEnvelope `json:"relationship,omitempty"`
Error string `json:"error,omitempty"`
}

type mcpProxyEvalOptions struct {
Expand Down Expand Up @@ -253,9 +258,15 @@ func evaluateMCPProxyPayload(policyPath string, payload []byte, options mcpProxy
resolvedTracePath = fmt.Sprintf("trace_%s_%s.json", normalizeRunID(options.RunID), time.Now().UTC().Format("20060102T150405.000000000"))
}
traceResult, err := gate.EmitSignedTrace(policy, evalResult.Intent, evalResult.Outcome.Result, gate.EmitTraceOptions{
ProducerVersion: version,
SigningPrivateKey: keyPair.Private,
TracePath: resolvedTracePath,
ProducerVersion: version,
ContextSource: evalResult.Outcome.ContextSource,
CompositeRiskClass: evalResult.Outcome.CompositeRiskClass,
StepVerdicts: evalResult.Outcome.StepVerdicts,
PreApproved: evalResult.Outcome.PreApproved,
PatternID: evalResult.Outcome.PatternID,
RegistryReason: evalResult.Outcome.RegistryReason,
SigningPrivateKey: keyPair.Private,
TracePath: resolvedTracePath,
})
if err != nil {
return mcpProxyOutput{}, exitInvalidInput, err
Expand Down Expand Up @@ -376,6 +387,9 @@ func evaluateMCPProxyPayload(policyPath string, payload []byte, options mcpProxy
ReasonCodes: evalResult.Outcome.Result.ReasonCodes,
Violations: evalResult.Outcome.Result.Violations,
PolicyDigest: traceResult.PolicyDigest,
PolicyID: traceResult.Trace.PolicyID,
PolicyVersion: traceResult.Trace.PolicyVersion,
MatchedRuleIDs: append([]string(nil), traceResult.Trace.MatchedRuleIDs...),
IntentDigest: traceResult.IntentDigest,
DecisionLatencyMS: decisionLatencyMS,
TraceID: traceResult.Trace.TraceID,
Expand All @@ -386,6 +400,7 @@ func evaluateMCPProxyPayload(policyPath string, payload []byte, options mcpProxy
LogExport: resolvedLogExport,
OTelExport: resolvedOTelExport,
Warnings: warnings,
Relationship: traceResult.Trace.Relationship,
}, exitCode, nil
}

Expand Down
50 changes: 41 additions & 9 deletions cmd/gait/mcp_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (

"github.com/Clyra-AI/gait/core/jobruntime"
"github.com/Clyra-AI/gait/core/runpack"
schemacommon "github.com/Clyra-AI/gait/core/schema/v1/common"
)

type mcpServeConfig struct {
Expand Down Expand Up @@ -502,16 +503,25 @@ func evaluateMCPServeRequest(config mcpServeConfig, writer http.ResponseWriter,
safetyInvariantHash = strings.TrimSpace(state.SafetyInvariantHash)
}
}
var agentChain []schemacommon.AgentLink
if output.Relationship != nil {
agentChain = append(agentChain, output.Relationship.AgentChain...)
}
event, err := runpack.AppendSessionEvent(journalPath, runpack.SessionAppendOptions{
ProducerVersion: version,
ToolName: output.ToolName,
IntentDigest: output.IntentDigest,
PolicyDigest: output.PolicyDigest,
TraceID: output.TraceID,
TracePath: output.TracePath,
Verdict: output.Verdict,
ReasonCodes: output.ReasonCodes,
Violations: output.Violations,
ProducerVersion: version,
ToolName: output.ToolName,
IntentDigest: output.IntentDigest,
PolicyDigest: output.PolicyDigest,
PolicyID: output.PolicyID,
PolicyVersion: output.PolicyVersion,
MatchedRuleIDs: output.MatchedRuleIDs,
TraceID: output.TraceID,
TracePath: output.TracePath,
AgentChain: agentChain,
ActorIdentity: mcpServeRelationshipCallActor(output.Relationship, output.ToolName),
Verdict: output.Verdict,
ReasonCodes: output.ReasonCodes,
Violations: output.Violations,
SafetyInvariantVersion: safetyInvariantVersion,
SafetyInvariantHash: safetyInvariantHash,
})
Expand Down Expand Up @@ -721,6 +731,28 @@ func sanitizeSessionFileBase(value string) string {
return strings.Trim(mapped, "._")
}

func mcpServeRelationshipCallActor(relationship *schemacommon.RelationshipEnvelope, toolName string) string {
if relationship == nil {
return ""
}
normalizedTool := strings.TrimSpace(toolName)
for _, edge := range relationship.Edges {
if strings.TrimSpace(edge.Kind) != "calls" {
continue
}
if strings.TrimSpace(edge.From.Kind) != "agent" || strings.TrimSpace(edge.To.Kind) != "tool" {
continue
}
if normalizedTool != "" && strings.TrimSpace(edge.To.ID) != normalizedTool {
continue
}
if actor := strings.TrimSpace(edge.From.ID); actor != "" {
return actor
}
}
return ""
}

func mcpServeErrorStatus(err error) int {
var requestErr mcpServeRequestError
if errors.As(err, &requestErr) {
Expand Down
14 changes: 10 additions & 4 deletions cmd/gait/voice.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,10 +175,16 @@ func runVoiceTokenMint(arguments []string) int {
return writeVoiceTokenOutput(jsonOutput, voiceTokenOutput{OK: false, Operation: "mint", Error: err.Error()}, exitCodeForError(err, exitInvalidInput))
}
traceResult, traceErr := gate.EmitSignedTrace(policy, normalizedIntent, outcome.Result, gate.EmitTraceOptions{
ProducerVersion: version,
CorrelationID: currentCorrelationID(),
SigningPrivateKey: keyPair.Private,
TracePath: strings.TrimSpace(tracePath),
ProducerVersion: version,
CorrelationID: currentCorrelationID(),
ContextSource: outcome.ContextSource,
CompositeRiskClass: outcome.CompositeRiskClass,
StepVerdicts: outcome.StepVerdicts,
PreApproved: outcome.PreApproved,
PatternID: outcome.PatternID,
RegistryReason: outcome.RegistryReason,
SigningPrivateKey: keyPair.Private,
TracePath: strings.TrimSpace(tracePath),
})
if traceErr != nil {
return writeVoiceTokenOutput(jsonOutput, voiceTokenOutput{OK: false, Operation: "mint", Error: traceErr.Error()}, exitCodeForError(traceErr, exitInvalidInput))
Expand Down
1 change: 1 addition & 0 deletions core/doctor/doctor.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ var requiredSchemaPaths = []string{
"schemas/v1/gate/approval_audit_record.schema.json",
"schemas/v1/gate/broker_credential_record.schema.json",
"schemas/v1/gate/approved_script_entry.schema.json",
"schemas/v1/common/relationship_envelope.schema.json",
"schemas/v1/context/envelope.schema.json",
"schemas/v1/context/reference_record.schema.json",
"schemas/v1/context/budget_report.schema.json",
Expand Down
1 change: 1 addition & 0 deletions core/gate/approval_audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ func BuildApprovalAuditRecord(opts BuildApprovalAuditOptions) schemagate.Approva
ValidApprovals: validApprovals,
Approved: validApprovals >= requiredApprovals,
Approvers: uniqueSorted(approvers),
Relationship: buildApprovalAuditRelationship(opts.TraceID, opts.ToolName, opts.PolicyDigest, approvers),
Entries: entries,
}
}
Expand Down
15 changes: 15 additions & 0 deletions core/gate/approval_audit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,21 @@ func TestBuildApprovalAuditRecordDeterministic(t *testing.T) {
if len(record.Entries) != 2 || record.Entries[0].TokenID != "token_a" {
t.Fatalf("expected sorted entries, got %#v", record.Entries)
}
if record.Relationship == nil {
t.Fatalf("expected relationship envelope in approval audit record")
}
if record.Relationship.ParentRef == nil || record.Relationship.ParentRef.Kind != "trace" || record.Relationship.ParentRef.ID != "trace_1" {
t.Fatalf("unexpected relationship parent_ref: %#v", record.Relationship.ParentRef)
}
if record.Relationship.PolicyRef == nil || record.Relationship.PolicyRef.PolicyDigest != record.PolicyDigest {
t.Fatalf("expected policy_ref digest in relationship: %#v", record.Relationship.PolicyRef)
}
if len(record.Relationship.AgentChain) != 0 {
t.Fatalf("expected no approver agent chain role in relationship: %#v", record.Relationship.AgentChain)
}
if len(record.Relationship.Edges) != 1 || record.Relationship.Edges[0].Kind != "governed_by" {
t.Fatalf("expected governed_by relationship edge in approval audit record: %#v", record.Relationship.Edges)
}
}

func TestWriteApprovalAuditRecord(t *testing.T) {
Expand Down
1 change: 1 addition & 0 deletions core/gate/delegation_audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ func BuildDelegationAuditRecord(opts BuildDelegationAuditOptions) schemagate.Del
ValidDelegations: validDelegations,
Delegated: validDelegations > 0,
DelegationRef: strings.TrimSpace(opts.DelegationRef),
Relationship: buildDelegationAuditRelationship(opts.TraceID, opts.ToolName, opts.PolicyDigest, entries),
Entries: entries,
}
}
Expand Down
12 changes: 12 additions & 0 deletions core/gate/delegation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,18 @@ func TestDelegationAuditRecordBuildAndWrite(t *testing.T) {
if len(record.Entries) != 2 || record.Entries[0].DelegatorIdentity != "agent.a" {
t.Fatalf("expected sorted delegation audit entries, got %#v", record.Entries)
}
if record.Relationship == nil {
t.Fatalf("expected relationship envelope in delegation audit record")
}
if record.Relationship.ParentRef == nil || record.Relationship.ParentRef.Kind != "trace" || record.Relationship.ParentRef.ID != "trace_demo" {
t.Fatalf("unexpected relationship parent_ref: %#v", record.Relationship.ParentRef)
}
if record.Relationship.PolicyRef == nil || record.Relationship.PolicyRef.PolicyDigest != record.PolicyDigest {
t.Fatalf("expected policy_ref digest in relationship: %#v", record.Relationship.PolicyRef)
}
if len(record.Relationship.Edges) == 0 {
t.Fatalf("expected relationship edges in delegation audit record")
}

workDir := t.TempDir()
path := filepath.Join(workDir, "audit", "delegation_audit.json")
Expand Down
1 change: 1 addition & 0 deletions core/gate/intent.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ func NormalizeIntent(input schemagate.IntentRequest) (schemagate.IntentRequest,
output.ArgProvenance = normalized.ArgProvenance
output.SkillProvenance = normalized.SkillProvenance
output.Delegation = normalized.Delegation
output.Relationship = normalizeRelationshipEnvelope(input.Relationship)
output.Context = normalized.Context
return output, nil
}
Expand Down
Loading
Loading