Documentation
¶
Index ¶
- Constants
- Variables
- func AgentCardFromConfig(cfg *types.ForgeConfig, baseURL string) *a2a.AgentCard
- func AgentCardFromSpec(spec *agentspec.AgentSpec, baseURL string) *a2a.AgentCard
- func AppendSkillsFromDescriptors(card *a2a.AgentCard, descs []contract.SkillDescriptor)
- func CapOrDefault(configured int) int
- func ContextBudgetForModel(model string) int
- func CorrelationIDFromContext(ctx context.Context) string
- func EnsureCorrelationID(ctx context.Context) context.Context
- func EnsureSequenceCounter(ctx context.Context) context.Context
- func ExtractText(msg *a2a.Message) string
- func FilesDirFromContext(ctx context.Context) string
- func GenerateID() string
- func HTTPEndpointForLog(s Sink) string
- func NextSequence(ctx context.Context) int64
- func ParseEnvVars(r io.Reader) (map[string]string, error)
- func PopulateSecuritySchemes(card *a2a.AgentCard, cfg *types.ForgeConfig)
- func PrepareCapturedContent(s string, redact bool, maxBytes int) string
- func PrepareSpanContent(s string, redact bool, maxBytes int) string
- func PublicKeyFromJWK(j JWK) (ed25519.PublicKey, error)
- func RedactSecrets(s string) string
- func ResetTracerProviderForTest()
- func SetTracerProvider(tp trace.TracerProvider)
- func StartAuditExportStatus(ctx context.Context, audit *AuditLogger) (stop func())
- func TaskIDFromContext(ctx context.Context) string
- func Tracer() trace.Tracer
- func TruncateForAudit(s string, max int) string
- func VerifySignature(pub ed25519.PublicKey, canonical []byte, sigB64 string) error
- func WithCorrelationID(ctx context.Context, id string) context.Context
- func WithFilesDir(ctx context.Context, dir string) context.Context
- func WithLLMUsageAccumulator(ctx context.Context, acc *LLMUsageAccumulator) context.Context
- func WithProgressEmitter(ctx context.Context, emitter ProgressEmitter) context.Context
- func WithSequenceCounter(ctx context.Context, c *SequenceCounter) context.Context
- func WithTaskID(ctx context.Context, id string) context.Context
- func WithTenancyContext(ctx context.Context, t TenancyContext) context.Context
- func WithWorkflowContext(ctx context.Context, w WorkflowContext) context.Context
- func WrapTransportForWorkflowPropagation(rt http.RoundTripper, matcher *WorkflowPropagationMatcher) http.RoundTripper
- type AdmissionChecker
- type AgentExecutor
- type AgentRuntime
- type AuditEvent
- type AuditExportConfig
- type AuditLogger
- func (a *AuditLogger) AddSink(s Sink)
- func (a *AuditLogger) Close(ctx context.Context) error
- func (a *AuditLogger) Emit(event AuditEvent)
- func (a *AuditLogger) EmitChannelDeniedByPolicy(channel, layer, source string)
- func (a *AuditLogger) EmitFromContext(ctx context.Context, event AuditEvent)
- func (a *AuditLogger) EmitInvocationCancelled(ctx context.Context, reason CancellationReason, duration time.Duration, ...)
- func (a *AuditLogger) EmitInvocationComplete(ctx context.Context, duration time.Duration, fields map[string]any)
- func (a *AuditLogger) EmitLLMCall(ctx context.Context, args LLMCallAuditArgs)
- func (a *AuditLogger) EmitPolicyLoaded(fields map[string]any)
- func (a *AuditLogger) EmitPolicyViolationAtBuildTime(fields map[string]any)
- func (a *AuditLogger) EmitToolExec(ctx context.Context, tool string, duration time.Duration, ...)
- func (a *AuditLogger) SetOpsLogger(l Logger)
- func (a *AuditLogger) SetSigner(s *AuditSigner)
- func (a *AuditLogger) Sinks() []Sink
- func (a *AuditLogger) WithEntity(entityType, entityID string) *AuditLogger
- func (a *AuditLogger) WithTenancy(orgID, workspaceID string) *AuditLogger
- type AuditPayloadCapture
- type AuditSigner
- type CancellationReason
- type CancellationRegistry
- type Compactor
- type CompactorConfig
- type Decision
- type DeferSpec
- type FallbackModelConfig
- type GuardrailChecker
- type Hook
- type HookContext
- type HookPoint
- type HookRegistry
- type JSONLogger
- type JWK
- type JWKS
- type LLMCallAuditArgs
- type LLMExecutor
- type LLMExecutorConfig
- type LLMUsage
- type LLMUsageAccumulator
- type LLMUsageSnapshot
- type LoadedKey
- type Logger
- type Memory
- type MemoryFlusher
- type MemoryStore
- type ModelConfig
- type NoopAdmissionChecker
- type NoopGuardrailChecker
- func (n *NoopGuardrailChecker) CheckContext(_ context.Context, content string) (string, error)
- func (n *NoopGuardrailChecker) CheckInbound(_ context.Context, _ *a2a.Message) (PolicyResult, error)
- func (n *NoopGuardrailChecker) CheckOutbound(_ context.Context, _ *a2a.Message) (PolicyResult, error)
- func (n *NoopGuardrailChecker) CheckStream(_ context.Context, chunk string) (string, error)
- func (n *NoopGuardrailChecker) CheckToolCall(_ context.Context, _, args string) (string, error)
- func (n *NoopGuardrailChecker) CheckToolOutput(_ context.Context, _ string, text string) (string, error)
- type PlatformCommandGuard
- type PlatformCommandMatch
- type PlatformCommandSpec
- type PolicyDecision
- type PolicyResult
- type ProgressEmitter
- type ProgressEvent
- type RemoteSessionStore
- type RemoteSessionStoreConfig
- type SequenceCounter
- type SequenceRegistry
- func (r *SequenceRegistry) Evict(correlationID, taskID string)
- func (r *SequenceRegistry) Get(correlationID, taskID string) *SequenceCounter
- func (r *SequenceRegistry) NextSequenceFor(correlationID, taskID string) int64
- func (r *SequenceRegistry) Register(correlationID, taskID string, c *SequenceCounter)
- type SessionData
- type SessionStore
- type Sink
- type SkillGuardrailEngine
- func (s *SkillGuardrailEngine) CheckCommandInput(toolName, toolInput string) error
- func (s *SkillGuardrailEngine) CheckCommandOutput(toolName, toolOutput string) (string, error)
- func (s *SkillGuardrailEngine) CheckLLMResponse(text string) (string, bool)
- func (s *SkillGuardrailEngine) CheckUserInput(text string) error
- type TenancyContext
- type ToolExecutor
- type VerifyOptions
- type VerifyResult
- type WorkflowContext
- type WorkflowPropagationMatcher
Constants ¶
const ( AuditSessionStart = "session_start" AuditSessionEnd = "session_end" AuditToolExec = "tool_exec" AuditEgressAllowed = "egress_allowed" AuditEgressBlocked = "egress_blocked" AuditLLMCall = "llm_call" AuditGuardrail = "guardrail_check" AuditScheduleFire = "schedule_fire" AuditScheduleComplete = "schedule_complete" AuditScheduleSkip = "schedule_skip" AuditScheduleModify = "schedule_modify" // Auth events. Carry no PII (no email, no claims, no token bytes) — // only the audit subject (UserID), tenant (OrgID), and structural // metadata. See the auth-package middleware for the emitter. EventAuthVerify = "auth_verify" // every successful auth decision EventAuthFail = "auth_fail" // every failed auth decision (with reason code) // AuditAuthStepUpRequired is emitted when the R4b (#210) step-up // engine rejects a tool call because the caller's acr claim // doesn't satisfy the tool's required acr. Fields carry: // - tool : the tool name // - required_acr : the operator-declared required acr // - presented_acr : the caller's current acr, or "" when absent // - reason : short human-readable classification // Payload never carries token bytes or full claim maps. The // runner also returns HTTP 401 with an RFC 9470 // WWW-Authenticate challenge alongside emitting this event. // See docs/security/step-up-auth.md. AuditAuthStepUpRequired = "auth_step_up_required" // MCP events. Like auth, these carry NO byte payload — never the // arguments to a tool call, never the result content. Emitters // include only sizes (args_size, result_size), durations, server + // tool names, and reason codes. See forge-core/mcp and // forge-core/tools/adapters/mcp_tool.go. EventMCPServerStarted = "mcp_server_started" EventMCPServerFailed = "mcp_server_failed" EventMCPServerDegraded = "mcp_server_degraded" EventMCPToolCall = "mcp_tool_call" EventMCPToolResult = "mcp_tool_result" EventMCPToolConflict = "mcp_tool_conflict" EventMCPTokenRefresh = "mcp_token_refresh" // Auth-required gate events (R10 / #330). A delegated (type=user) MCP // call with no grant for the requesting user PARKS on the authgate // engine instead of failing; these trace park → resume/timeout. The // gate never sees a token — it only unblocks the executor to re-resolve // once the platform holds a grant. // // EventMCPAuthRequired — a call parked awaiting user consent; the // task flipped to `auth-required` and (first // waiter only) a consent prompt was delivered. // EventMCPAuthResolved — consent arrived (or the wait was canceled); // parked call(s) resumed and re-resolved. // EventMCPAuthTimeout — no consent within the window; the call fails. EventMCPAuthRequired = "mcp_auth_required" EventMCPAuthResolved = "mcp_auth_resolved" EventMCPAuthTimeout = "mcp_auth_timeout" // Agent Card events. Emitted once at agent startup with the // finalized A2A Agent Card content for traceability. Carries the // card's name, version, URL, protocolVersion, skill count, and a // sha256 hash of the JSON-encoded card so consumers can detect // config drift. See forge-cli/runtime/runner.go's startup pass // and the A2A 0.3.0 spec. EventAgentCardPublished = "agent_card_published" // Lifecycle events emitted at A2A invocation boundaries. // AuditInvocationComplete carries total wall-clock duration_ms for // the full invocation (auth → dispatch → engine.Execute → response). // See issue #87 / FWS-3. AuditInvocationComplete = "invocation_complete" // AuditLLMCallCancelled is emitted when a streaming LLM call is // cancelled mid-flight; carries partial usage counts captured up to // the cancellation point. See issue #87 / FWS-3. AuditLLMCallCancelled = "llm_call_cancelled" // AuditLLMCallFailed records a provider/gateway-rejected or errored LLM // call (#361). Without it a failing call is invisible to the audit // stream — an agent whose every task 400s at the provider showed // nothing but healthy pre-failure llm_call rows (field-hit 2026-07-22). AuditLLMCallFailed = "llm_call_failed" // Credential events (governance R9). Emitted per BeforeToolExec // when a JIT credential is materialized for a tool call, and again // on AfterToolExec when a revocable credential is revoked. Fields: // provider (plugin name), tool, ttl, scope. Payloads never carry // the credential material itself — only its metadata. AuditCredentialIssued = "credential_issued" AuditCredentialRevoked = "credential_revoked" AuditCredentialFailed = "credential_failed" // AuditIntentAlignment is emitted per BeforeToolExec when the R3 // intent-alignment engine (#208) is enabled. Fields carry: // - tool : the tool name being scored // - score : cosine similarity ∈ [-1, 1] (or "NaN" on error) // - decision : "allow" / "warn" / "deny" // - reason : short human-readable classification // Payload never carries the LLM prompt or tool args — only the // scored decision. See docs/security/intent-alignment.md. AuditIntentAlignment = "intent_alignment" // AuditIntentDrift is emitted when the R7 (#214) rolling-window // drift analyzer detects a state transition — either the task // entered drift (mean-below-threshold OR monotone-decrease) or // recovered. State-transition semantics keep the audit stream // from flooding during a long drift stretch: one event on entry, // one on recovery. // // Fields: // - tool : the tool call that tripped the transition // - severity : "mean_below_threshold" | "monotone_decrease" | // "both" | "recovered" // - transition : "entered" | "recovered" // - mean : rolling-window mean at trip time // - window : the configured window size // Payload never carries the individual scores. See // docs/security/intent-alignment.md (drift section). AuditIntentDrift = "intent_drift" // Defer events (governance R4c / #211). Three-stage lifecycle: // 1. AuditTaskDeferred — hook paused the executor, // task flipped to `deferred` // state. Fields: tool, to, // timeout_ms, context (truncated). // 2. AuditTaskDeferredDecision — POST /tasks/{id}/decisions // arrived. Fields: tool, // decision (approve|reject), // approver, note, wait_ms. // 3. AuditTaskDeferredTimeout — no decision arrived within // timeout, auto-DENYing. // Fields: tool, timeout_ms. // Payload never carries token bytes or full LLM messages. AuditTaskDeferred = "task_deferred" AuditTaskDeferredDecision = "task_deferred_decision" AuditTaskDeferredTimeout = "task_deferred_timeout" // AuditPolicyLoaded is emitted once at agent startup when a // non-zero platform policy is present. Carries a summary of the // effective policy (sizes of deny lists, max bounds) so audit // consumers can confirm which policy was active during a given // run without parsing the policy file itself. Absent when no // policy is configured. See issue #89 / FWS-5. AuditPolicyLoaded = "policy_loaded" // AuditPolicyViolationAtBuildTime is emitted when forge.yaml's // declaration conflicts with the platform policy at startup // (e.g., declares a domain on the policy deny list, declares a // forbidden model, exceeds size bounds). Carries the conflict // detail in Fields. Emitted ONCE per startup before the runner // aborts with a non-zero exit, so the violation lands in the // audit pipeline even though the agent never serves traffic. // See issue #89 / FWS-5. AuditPolicyViolationAtBuildTime = "policy_violation_at_build_time" // AuditChannelDeniedByPolicy is emitted at agent startup when a // channel adapter would have been started but a policy layer // (system / user / workspace) names it on its denied_channels // list. The channel is NOT started; the runner continues with // the remaining channels rather than aborting — unlike the // egress/tool/model violations, channel deny is treated as a // scope-down, not as a forge.yaml conflict. // // Carries fields.channel (registry name), fields.layer // ("system" / "user" / "workspace") identifying which file // enforced, and fields.source (path to that file). When the // channel is denied by multiple layers, the first-match wins // for attribution (system > user > workspace precedence; the // most restrictive layer takes credit). // // See issue #90 / FWS-6. AuditChannelDeniedByPolicy = "channel_denied_by_policy" // AuditInvocationCancelled is emitted when an in-flight A2A // invocation is cancelled by tasks/cancel (or internal cancellation // like a parent ctx deadline). Carries the classified reason in // Fields["reason"], the wall-clock duration up to cancellation in // DurationMs, and aggregated partial usage in Fields when any LLM // calls completed before the cancel signal. See issue #88 / FWS-4. AuditInvocationCancelled = "invocation_cancelled" // AuditTaskAdmissionDenied is emitted when the admission middleware // rejects an inbound A2A invocation based on a platform-side quota // / cost-limit decision (issue #201). Carries the platform's // classification in Fields: // // - reason : platform-defined failure code // ("cost_limit_exceeded", "billing_overdue", …) // - scope : which level in the platform's hierarchy tripped // ("agent" / "workspace" / "org") // - window : which quota window tripped // ("hourly" / "daily" / "monthly" / "billing_cycle") // - reset_at: RFC 3339 timestamp when the deny clears, also used // to derive the caller's Retry-After header // - cached : true when the decision came from Forge's per-agent // TTL cache; false on a fresh platform call. Lets // operators distinguish "platform actively denied" // from "serving a few-second-old cached deny" when // debugging propagation lag. // // Caller observes HTTP 402 Payment Required with the same shape as // the audit fields surfaced in the response body. Distinct from // auth_fail (which signals authentication failure, HTTP 401) and // from rate limit drops (which signal request-rate ceilings, HTTP // 429). See docs/security/admission.md. AuditTaskAdmissionDenied = "task_admission_denied" // AuditPDPDecision is emitted for EVERY managed PDP verdict at // BeforeToolExec (the per-tool-call authorization altitude, over // argument values — distinct from task_admission_denied's per-invocation // quota gate). Carries in Fields: tool (runtime name), op (registry // operation), decision (allow|defer|deny), reason, caller, agent. A deny // is a permanent-log deliverable. The agent-side enforcement record; the // platform separately records its own decision as tool_call_decided. AuditPDPDecision = "pdp_decision" // Deprecated: use EventAuthVerify. Kept as a string alias so any // audit-log consumer that grep'd for "auth_success" can be migrated. // Scheduled for removal in v0.11.0. AuditAuthSuccess = EventAuthVerify // Deprecated: use EventAuthFail. Same migration window as AuditAuthSuccess. AuditAuthFailure = EventAuthFail )
Audit event type constants.
const ( EnvAuditSocket = "FORGE_AUDIT_SOCKET" EnvAuditHTTPEndpoint = "FORGE_AUDIT_HTTP_ENDPOINT" EnvAuditWriteTimeout = "FORGE_AUDIT_WRITE_TIMEOUT" )
Environment variable names. Exposed for `forge run --help` text and for the integration test. The CLI in forge-cli/cmd/run.go reads these and surfaces matching --audit-* flags; flag wins over env.
const ( EnvAuditCaptureToolArgs = "FORGE_AUDIT_CAPTURE_TOOL_ARGS" EnvAuditCaptureToolResult = "FORGE_AUDIT_CAPTURE_TOOL_RESULT" EnvAuditCaptureLLMMessages = "FORGE_AUDIT_CAPTURE_LLM_MESSAGES" EnvAuditCaptureLLMResponse = "FORGE_AUDIT_CAPTURE_LLM_RESPONSE" EnvAuditCaptureRedact = "FORGE_AUDIT_CAPTURE_REDACT" EnvAuditCaptureMaxBytes = "FORGE_AUDIT_CAPTURE_MAX_BYTES" )
Environment variable names for AuditPayloadCaptureFromEnv. Mirrors the AuditExportConfig / GuardrailAuditConfig env naming so operators see one consistent `FORGE_AUDIT_*` namespace across the audit subsystem. See issue #163.
const ( HeaderForgeOrgID = "X-Forge-Org-ID" HeaderForgeWorkspaceID = "X-Forge-Workspace-ID" )
Tenancy header names (issue #157). The X-Forge- prefix is deliberate: these are Forge-defined override headers, distinct from the X-Org-ID / org_id headers the auth providers parse to resolve the user's identity. The auth-derived org_id continues to live in auth_verify.fields.org_id for back-compat; these headers populate the top-level audit fields that get stamped on EVERY event.
Header semantics:
- Absent: the AuditLogger's deployment-time stamp wins (env vars FORGE_ORG_ID / FORGE_WORKSPACE_ID resolved at startup). This is the static-tenancy case — agent deployed into one workspace, no per-request routing.
- Present: the header value overrides the env stamp for that invocation. This is the multi-tenant case — one Forge agent serves many workspaces, the orchestrator routes per request.
Both: header wins. Neither: top-level fields are omitted entirely and emitted JSON matches the pre-tenancy shape.
const ( HeaderWorkflowID = "X-Workflow-ID" HeaderWorkflowExecutionID = "X-Workflow-Execution-ID" HeaderWorkflowStageID = "X-Workflow-Stage-ID" HeaderWorkflowStepID = "X-Workflow-Step-ID" HeaderInvocationCaller = "X-Invocation-Caller" )
Workflow correlation header names (issue #86 / FWS-2). Sent by any A2A-compatible orchestrator on every request that's part of a workflow execution. Header names are deliberately vendor-neutral so any orchestrator (initializ Command, custom registries, third-party platforms) can drive Forge's correlation surface without adopting a vendor prefix. Forge agents extract them at the request boundary, stash them in context.Context, and tag every audit event with the matching workflow / execution / stage / step identifiers so audit consumers can correlate events across multiple agents participating in the same workflow.
FORGE-2 / issue #185 split the previously-overloaded X-Workflow-ID header into two distinct identifiers so operators can answer both "show me this specific run" (per-execution) and "show me every run of this workflow" / "top failing workflows" (per-definition) queries without a join on opaque ids:
- HeaderWorkflowID: workflow DEFINITION id, stable across all runs of the same workflow.
- HeaderWorkflowExecutionID: PER-RUN instance id, unique per workflow execution.
Industry precedent for the split: GitHub Actions (workflow + workflow_run_id), Tekton (Pipeline + PipelineRun), Argo (Workflow + WorkflowRun).
Absence of these headers is the normal case for direct A2A invocations (e.g. local development, peer agents not orchestrated). When absent, audit events emit without the workflow fields — full backward compatibility with pre-FWS-2 audit consumers.
const AuditChainGenesis = "0000000000000000000000000000000000000000000000000000000000000000"
AuditChainGenesis is the `prev_hash` value written on the very first audit event of a process. Downstream verifiers recognize this as "no predecessor" — sha256 (32 zero bytes) rendered in hex. See docs/security/audit-tamper-evidence.md (governance R5, issue #212).
const AuditExportStatus = "audit_export_status"
AuditExportStatus is the event type for the per-sink health report.
const AuditSchemaVersion = "1.0"
AuditSchemaVersion is the current audit event contract version. Every emitted event carries this string in its `schema_version` field so consumers can detect schema upgrades. Backward-compatible additions (new optional fields) do NOT bump the version; removals or semantic changes do.
Version policy:
1.0 — initial documented contract (issue #91 / FWS-8). Includes the
pre-FWS-8 fields (ts, event, correlation_id, task_id, workflow_*,
model, provider, input_tokens, output_tokens, duration_ms,
request_id, fields) plus seq and schema_version.
See docs/security/audit-logging.md for the full schema reference.
const CompressionMarkerPrefix = "<<ctxzip:"
CompressionMarkerPrefix mirrors ctxzip's ccr.MarkerPrefix. The runtime cannot import forge-core/compress (it imports runtime — cycle), so the literal is pinned here and a guard test in the compress package asserts the two stay equal.
const DefaultPayloadCaptureCapBytes = 16 << 10
DefaultPayloadCaptureCapBytes is the per-field byte cap when the caller doesn't override. 16 KiB matches the runtime's "long-tool-output" threshold (the same threshold the chat-side path uses to switch to a file part), so audit captures roughly align with what's visible in the chat UI.
const DefaultSpanContentCapBytes = 4 << 10
DefaultSpanContentCapBytes is the per-attribute byte cap for span content. 4 KiB stays comfortably under common observability backend limits (Datadog caps attributes around 5 KiB; Tempo's default attr length limit is 4 KiB) so a long prompt doesn't get re-truncated by the backend with a different marker shape, breaking the correlate-by-marker grep flow.
const EnvAuditStatusKeepaliveInterval = "AUDIT_STATUS_KEEPALIVE_INTERVAL"
EnvAuditStatusKeepaliveInterval overrides AuditExportStatusKeepaliveInterval when set to a valid Go duration. Read once at startup (deploy-time knob, not live-tunable).
const InstrumentationName = "github.com/initializ/forge"
InstrumentationName is the OTel instrumentation scope name used for every tracer Forge obtains. Pinned to the module path so OTel backends can distinguish Forge spans from spans emitted by other instrumentation in the same process. Subsequent phases (#102–#107 in the OTel v1 initiative, #108) read this same constant — do not duplicate it.
const ProtocolVersion = "0.3.0"
ProtocolVersion is the A2A protocol version every AgentCard claims to conform to. Pinned at build time. Bumping is a deliberate PR, not a runtime negotiation — same discipline Forge uses for the MCP protocol version pin.
const RedactionMarker = "[REDACTED]"
RedactionMarker is the placeholder substituted for any matched secret. Operators grepping audit logs and traces for "[REDACTED]" can correlate scrub events across both pipelines.
const SigCanonicalizationJCS1 = "jcs-1"
SigCanonicalizationJCS1 is the value stamped on AuditEvent.Sigp when the signature preimage is RFC 8785 JCS canonical form. See canonicalBytesForSigning.
Variables ¶
var ( // AuditExportStatusPollInterval is how often sink health is checked for a // state change. This is the failure-detection latency. Package var so // tests can shorten it. AuditExportStatusPollInterval = 15 * time.Second // AuditExportStatusKeepaliveInterval is the maximum time between emitted // status events when nothing changes. Overridable at process startup via // the AUDIT_STATUS_KEEPALIVE_INTERVAL env var (a Go duration, e.g. "15m", // "1h"); it is read once when the heartbeat starts, so a change takes a // restart — there is no live reload. Package var so tests can shorten it. AuditExportStatusKeepaliveInterval = 15 * time.Minute )
Audit-export status heartbeat cadence (issue #280). The event proves the audit pipeline is alive and reports per-sink health. Emitting it once a minute produced ~1,440 rows/agent/day of near-constant "still fine" noise that dominated the audit collection at fleet scale. The hybrid below keeps the integrity signal while collapsing steady-state volume to a handful/day:
- poll sink health frequently and emit IMMEDIATELY on a state change (a sink's `connected` flag flips, or a sink is added/removed), so a failure surfaces within one poll interval;
- otherwise emit a keepalive on a slow interval, so liveness stays provable (a missing keepalive past the interval is itself alertable).
The edge signal is `connected` — a LEVEL the sink maintains — and NOT the cumulative `drops_*` counters. This is deliberate and load-bearing: the status event itself flows through every sink, including a failing one, so its own write bumps that sink's drop counter. On an idle agent the status emits are the *only* writes, so a drop-delta edge would be self-referential — every poll would see "drops increased since last time" and emit again, one event per poll for the whole outage (4× the old fixed cadence, and worst precisely during an incident). `connected` has no such feedback: a dial failure holds it at 0 and a write timeout disconnects the sink (so it also reads 0 on the next poll), so both failure modes converge on a single 1→0 transition that settles until recovery flips it back. Drop counters still ride in every emit's `sinks[]` payload for anyone tracking totals; they just don't drive the edge.
Every emit carries fields.reason ("state_change" | "keepalive") so consumers can distinguish "something changed" from "still alive".
var ErrConflict = errors.New("session store: version conflict")
ErrConflict is returned by a SessionStore.Save when the store's current version no longer matches the version the caller last observed — an optimistic-concurrency (compare-and-swap) failure. The remote backend attempts one rebase-and-retry before surfacing this; a caller that still sees it should treat the persist as best-effort-failed (never re-run the LLM), matching the existing "failed to persist session" posture.
var ModelContextWindows = map[string]int{
"gpt-4o": 128_000,
"gpt-4": 128_000,
"gpt-5": 128_000,
"gpt-3.5": 16_000,
"claude-opus": 200_000,
"claude-sonnet": 200_000,
"claude-haiku": 200_000,
"gemini-2.5": 1_000_000,
"gemini-2.0": 1_000_000,
"llama3.1": 128_000,
"llama3": 8_000,
"mistral": 32_000,
"codellama": 16_000,
"deepseek": 64_000,
"qwen": 32_000,
}
ModelContextWindows maps model name prefixes to context window sizes (in tokens).
Functions ¶
func AgentCardFromConfig ¶
func AgentCardFromConfig(cfg *types.ForgeConfig, baseURL string) *a2a.AgentCard
AgentCardFromConfig constructs an AgentCard from a ForgeConfig and a base URL. The baseURL should be a fully-formed URL (e.g. "http://localhost:8080").
Used when no build-time AgentSpec is available (e.g. local `forge dev` from a freshly-scaffolded project). Identical conformance to A2A 0.3.0 as the spec-derived path.
func AgentCardFromSpec ¶
AgentCardFromSpec constructs an AgentCard from an AgentSpec and a base URL. The baseURL should be a fully-formed URL (e.g. "http://localhost:8080").
Per A2A 0.3.0 the card requires `version`, `protocolVersion`, `defaultInputModes`, and `defaultOutputModes`. The function fills those from the spec / config defaults; callers can override after construction (e.g. the runner enriches with SecuritySchemes derived from the auth chain).
func AppendSkillsFromDescriptors ¶
func AppendSkillsFromDescriptors(card *a2a.AgentCard, descs []contract.SkillDescriptor)
AppendSkillsFromDescriptors maps the runtime's SkillDescriptor list (sourced from the embedded + local skill registries) into A2A AgentSkill objects and appends them to the card. Skill IDs already present on the card are skipped so this is safe to call after AgentCardFromSpec / AgentCardFromConfig — those populate the card from build-time artifacts; this fills in any runtime-registered skills the build artifact didn't cover.
Mapping (Forge SKILL.md → A2A AgentSkill):
SkillDescriptor.Name → Skill.ID + Skill.Name SkillDescriptor.DisplayName → Skill.Name (when present) SkillDescriptor.Description → Skill.Description SkillDescriptor.Category → Skill.Tags[0] (when present) SkillDescriptor.Tags → Skill.Tags (appended)
A2A 0.3.0 requires Tags to be non-empty; when neither category nor tags are set, we fall back to ["skill"] so the field is always populated.
Forge-internal fields (RequiredEnv, RequiredBins, EgressDomains, DeniedTools, TimeoutHint, Provenance) are intentionally NOT mapped into the card. The Agent Card is a public discovery surface; those fields are runtime contracts that stay inside Forge.
func CapOrDefault ¶
CapOrDefault picks the configured cap for the field, falling back to the package default when zero. Negative values are clamped to the default; "0 means no capture" is what AnyEnabled / the per- field flag covers — once a flag is on, some cap applies. Exported so the runner's hook layer can pick the right cap per field without duplicating the fallback logic.
func ContextBudgetForModel ¶
ContextBudgetForModel returns the character budget for a given model name. Uses prefix matching against known models, falls back to defaultContextTokens. Prefixes are checked longest-first to avoid e.g. "llama3" matching before "llama3.1".
func CorrelationIDFromContext ¶
CorrelationIDFromContext retrieves the correlation ID from the context. Returns "" if not set.
func EnsureCorrelationID ¶ added in v0.17.0
EnsureCorrelationID returns a context guaranteed to carry a correlation ID (the per-invocation identifier). If one is already present — e.g. minted at request INGRESS before the auth chain ran — it is preserved, so pre-admission events (auth_verify/auth_fail) and the task events that follow in the same request share one invocation id. Only when none exists (a background path such as a schedule fire, or a direct test invocation) is a fresh one generated. Mirrors EnsureSequenceCounter; issue #278.
func EnsureSequenceCounter ¶ added in v0.16.0
EnsureSequenceCounter returns ctx unchanged when it already carries a SequenceCounter; otherwise it returns a new ctx with a fresh counter installed. Use at any invocation-entry point that may run downstream of an upstream middleware which already installed a counter — e.g., the runner's per-A2A-request setup runs after the auth middleware (which installs a counter so auth_verify lands seq=1) and must not clobber it. See issue #174.
func ExtractText ¶
ExtractText projects a message into the string the inbound guardrail and intent-alignment checks scan. It delegates to Message.PromptText so the scanners see exactly what the LLM sees — including data parts (#410), so a prompt-injection or misaligned payload carried in a data part can't slip past the checks while still reaching the model.
func FilesDirFromContext ¶
FilesDirFromContext retrieves the files directory from the context. Returns "" if not set.
func GenerateID ¶
func GenerateID() string
GenerateID produces a 16-character hex random ID using crypto/rand.
func HTTPEndpointForLog ¶
HTTPEndpointForLog is exposed so the runner's startup banner can log "exporting audit to <endpoint>" without forcing the runner to reach into a private field. Returns the endpoint URL.
func NextSequence ¶
NextSequence increments the per-invocation counter and returns the new value. Atomic; safe to call from multiple goroutines without external synchronization. Returns 0 when no counter is in context (caller can JSON-omit the field).
func ParseEnvVars ¶
ParseEnvVars reads key=value pairs from an io.Reader. Supports # comments, double/single quotes, and export prefix.
func PopulateSecuritySchemes ¶
func PopulateSecuritySchemes(card *a2a.AgentCard, cfg *types.ForgeConfig)
PopulateSecuritySchemes derives the A2A SecuritySchemes + Security requirements from the agent's configured auth chain (forge.yaml auth: block) and writes them into the card. The function is additive: it preserves any schemes the caller has already set.
The mapping mirrors what Forge's auth middleware actually accepts:
static_token → http + bearer (opaque token)
http_verifier → http + bearer (token validated by external endpoint)
oidc → openIdConnect with issuer discovery URL
azure_ad → openIdConnect (AAD exposes a standard OIDC discovery)
gcp_iap → apiKey in header (X-Goog-Iap-Jwt-Assertion)
aws_sigv4 → http + bearer with bearerFormat "forge-aws-v1"
(forge-specific Sigv4-reflection-via-bearer pattern)
Every chain entry produces one scheme name = entry's Name field (or Type when Name is empty). The Security array carries one map per scheme; the outer list is OR (any one suffices), matching Forge's first-match-wins chain semantics.
When no auth is configured, no schemes are emitted — A2A 0.3.0 treats absence as "no auth required."
func PrepareCapturedContent ¶ added in v0.16.0
PrepareCapturedContent is the shared redact-then-truncate pipeline for any content the runtime captures into a long-lived artifact — audit events (FWS-8 payload capture), OTel span attributes (#130 content capture), guardrail evidence (#155 / #156). All three call sites previously open-coded a near-identical redact + truncate flow with three independent copies of the vendor-secret regex set; this helper consolidates them onto one implementation so a fix to the regex set propagates to every capture path (issue #163).
Pipeline:
- Empty input is returned unchanged (fast path; callers can use the empty return as the signal to drop the field cleanly).
- If redact=true, RedactSecrets scrubs known vendor token shapes.
- maxBytes <= 0 falls back to DefaultPayloadCaptureCapBytes (16 KiB). Each call site that wants a different default (the span path uses DefaultSpanContentCapBytes=4 KiB; the guardrail evidence path uses DefaultGuardrailEvidenceCapBytes=4 KiB) MUST pass its own explicit cap.
- TruncateForAudit applies the byte cap and writes the `…[truncated:N]` marker the audit and OTel paths share.
Order matters: redact runs BEFORE truncate so the truncation boundary can never split a `[REDACTED]` marker mid-string.
func PrepareSpanContent ¶
PrepareSpanContent is the OTel-span-attribute-specific adapter over PrepareCapturedContent. It applies the span-attribute default cap (DefaultSpanContentCapBytes, 4 KiB — comfortably under common observability backend per-attribute limits) when the caller passes maxBytes <= 0. Behavior is otherwise identical to PrepareCapturedContent and the truncation marker is shared so an operator correlating an audit `…[truncated:N]` substring with the linked span attribute sees the same suffix on both.
Issue #130 shipped this helper; issue #163 collapses it onto PrepareCapturedContent so all three content-capture paths share one implementation.
func PublicKeyFromJWK ¶ added in v0.17.0
PublicKeyFromJWK extracts an Ed25519 public key from a JWK. Used by the verifier when a pubkey file is supplied on the command line.
func RedactSecrets ¶
RedactSecrets returns s with every known secret token shape replaced by RedactionMarker. Empty input is returned unchanged (fast path).
Applied in pattern-list order; overlap is fine because ReplaceAllString rewrites the string left-to-right and subsequent patterns operate on the post-replacement output. A run that matches multiple shapes (e.g. an `sk-` prefix that also starts a longer vendor key) is scrubbed once — RedactionMarker doesn't satisfy any other pattern, so re-applying patterns is idempotent.
func ResetTracerProviderForTest ¶
func ResetTracerProviderForTest()
ResetTracerProviderForTest restores the no-op provider. Exists so tests that install a real provider can cleanly tear down. Not part of the production wiring — kept exported so tests in the forge-core/observability subpackage (Phase 1) can also use it.
func SetTracerProvider ¶
func SetTracerProvider(tp trace.TracerProvider)
SetTracerProvider installs the given TracerProvider as the process-wide tracer source for forge-core. Also installs it as the OTel global so any third-party library Forge depends on (the OTLP exporter's own transport, future runtime-loaded SDKs) sees the same provider when it calls otel.Tracer() directly. Also installs the W3C tracecontext + baggage composite propagator as the OTel global text-map propagator so inbound/outbound HTTP plumbing (Phase 5, #106) can extract / inject trace context without further setup.
Calling SetTracerProvider with nil is a no-op (defensive guard for the cli wiring path — a misconfigured exporter resolution must not install a nil provider that would crash on the first Tracer call).
Safe to call from any goroutine. Subsequent calls replace the previous provider; intended to fire exactly once at agent startup.
func StartAuditExportStatus ¶
func StartAuditExportStatus(ctx context.Context, audit *AuditLogger) (stop func())
StartAuditExportStatus spawns a background goroutine that emits audit_export_status events using the hybrid cadence described above, until the returned stop function is called or ctx is cancelled. The goroutine uses the same AuditLogger it reports on — the status event flows through every sink (including the export ones) so operators can see "is my export healthy?" by inspecting the export stream.
An initial keepalive is emitted at startup so liveness is provable from t=0. The stop func blocks until the goroutine exits (deterministic shutdown ordering) and is idempotent.
See issue #95 / FWS-7 §6 (original) and issue #280 (hybrid cadence).
func TaskIDFromContext ¶
TaskIDFromContext retrieves the task ID from the context. Returns "" if not set.
func Tracer ¶
Tracer returns a tracer scoped to the Forge instrumentation name. When no provider has been installed, the returned tracer is the no-op tracer — every span it produces has IsValid() == false and records nothing. Hot-path code calls Tracer().Start unconditionally; the no-op short-circuit is cheap by design.
func TruncateForAudit ¶
TruncateForAudit returns s truncated to at most max bytes; if s exceeded the cap, the returned string ends with the suffix `…[truncated:N]` where N is the original byte length. Use for every captured field so a runaway prompt can't bloat one event.
The function operates on bytes, not runes — UTF-8 sequences may be split mid-codepoint at the truncation boundary. Audit consumers must treat captured strings as opaque bytes, not as user-renderable text. The size info in the field name (`prompt_messages_size_bytes` vs `prompt_messages`) is the contract.
func VerifySignature ¶ added in v0.17.0
VerifySignature checks the base64 signature over `canonical` bytes with the given pubkey. Returns nil on match, non-nil otherwise.
func WithCorrelationID ¶
WithCorrelationID stores a correlation ID in the context.
func WithFilesDir ¶
WithFilesDir stores a files directory path in the context.
func WithLLMUsageAccumulator ¶
func WithLLMUsageAccumulator(ctx context.Context, acc *LLMUsageAccumulator) context.Context
WithLLMUsageAccumulator stashes a per-invocation accumulator in ctx. The runner creates one per A2A invocation at request entry; the AfterLLMCall hook reads it via LLMUsageAccumulatorFromContext and folds each call's counts into the totals.
func WithProgressEmitter ¶
func WithProgressEmitter(ctx context.Context, emitter ProgressEmitter) context.Context
WithProgressEmitter stores a ProgressEmitter in the context.
func WithSequenceCounter ¶
func WithSequenceCounter(ctx context.Context, c *SequenceCounter) context.Context
WithSequenceCounter stores a per-invocation sequence counter in the context. Called by the A2A request entry point exactly once per invocation; every audit emit downstream picks the counter up via SequenceCounterFromContext. Events emitted outside an invocation scope (startup banners, policy_loaded) inherit no counter and emit with Sequence == 0 (which JSON-omits via omitempty).
func WithTaskID ¶
WithTaskID stores a task ID in the context.
func WithTenancyContext ¶
func WithTenancyContext(ctx context.Context, t TenancyContext) context.Context
WithTenancyContext stores a TenancyContext in the request context. Called at the A2A request boundary right after the workflow context is installed, so per-invocation handlers and the downstream audit emitters see both.
func WithWorkflowContext ¶
func WithWorkflowContext(ctx context.Context, w WorkflowContext) context.Context
WithWorkflowContext stores a WorkflowContext in the request context. Mirrors the WithCorrelationID / WithTaskID pattern already used by the audit layer.
func WrapTransportForWorkflowPropagation ¶ added in v0.16.0
func WrapTransportForWorkflowPropagation(rt http.RoundTripper, matcher *WorkflowPropagationMatcher) http.RoundTripper
WrapTransportForWorkflowPropagation wraps an existing RoundTripper so requests targeting allow-listed hosts auto-receive workflow headers from the request context. Returns the underlying transport unchanged when the matcher is empty — the default-deploy zero- overhead path (no extra goroutine hops, no extra allocations per request when nothing is configured).
Hook this from the runner once, around the egress transport, before the resulting client is stashed onto the request context via security.WithEgressClient — every HTTP tool that reads the egress transport from context (which is all of them) picks up the auto-apply transparently. Issue #186.
Types ¶
type AdmissionChecker ¶ added in v0.16.0
AdmissionChecker decides whether a new inbound A2A invocation should be admitted. Used by the admission middleware (issue #201) to gate `tasks/send` (and any other write-method) traffic on a per-agent quota / cost ceiling that the platform owns.
Forge itself never reasons about WHY a call is denied — it asks the platform a single yes-or-no question per agent and surfaces the answer to the caller. The platform's `Decision.Reason` / `Decision.Scope` / `Decision.Window` fields ride through to the caller's response body and to the `task_admission_denied` audit event so SIEM consumers can group by failure mode without joining against platform-side state.
Admit MUST be safe to call concurrently; production checkers serve many in-flight A2A requests at once.
type AgentExecutor ¶
type AgentExecutor interface {
// Execute processes a message in the context of a task and returns a response.
Execute(ctx context.Context, task *a2a.Task, msg *a2a.Message) (*a2a.Message, error)
// ExecuteStream processes a message and returns a channel of response messages.
ExecuteStream(ctx context.Context, task *a2a.Task, msg *a2a.Message) (<-chan *a2a.Message, error)
// Close releases any resources held by the executor.
Close() error
}
AgentExecutor processes individual messages and returns responses. Unlike AgentRuntime (which manages subprocess lifecycle), the executor focuses solely on message-level processing. The handler (runner.go) manages task lifecycle (submitted -> working -> completed/failed).
type AgentRuntime ¶
type AgentRuntime interface {
// Start launches the agent backend.
Start(ctx context.Context) error
// Invoke sends a synchronous task request and returns the completed task.
Invoke(ctx context.Context, taskID string, msg *a2a.Message) (*a2a.Task, error)
// Stream sends a streaming task request and returns a channel of task updates.
Stream(ctx context.Context, taskID string, msg *a2a.Message) (<-chan *a2a.Task, error)
// Healthy reports whether the agent backend is responsive.
Healthy(ctx context.Context) bool
// Stop shuts down the agent backend.
Stop() error
// Restart stops and restarts the agent backend.
Restart(ctx context.Context) error
}
AgentRuntime abstracts the agent execution backend. Implementations include SubprocessRuntime (real agent process) and MockRuntime (canned responses).
type AuditEvent ¶
type AuditEvent struct {
Timestamp string `json:"ts"`
Event string `json:"event"`
// SchemaVersion advertises the audit-event contract version every
// emitted event conforms to. Consumers (initializ platform,
// custom SIEM pipelines) read this once per agent run to detect
// schema upgrades. Backward-compatible additions to the schema
// do not bump the version; removals or semantic changes do.
// See docs/security/audit-logging.md#schema-contract-fws-8.
SchemaVersion string `json:"schema_version,omitempty"`
// Sequence is a per-invocation monotonically increasing counter.
// Starts at 1 for the first event of an invocation; advances by
// 1 on each subsequent event from that invocation. Consumers
// detect gaps (lost / out-of-order events) by comparing
// Sequence values within a (correlation_id, task_id) group.
//
// Sequences are scoped to a single A2A invocation — different
// invocations start their own counters. Events emitted outside
// any invocation scope (startup events: policy_loaded,
// agent_card_published, audit_export_status) have no Sequence
// and omit the field.
//
// See issue #91 / FWS-8.
Sequence int64 `json:"seq,omitempty"`
// PrevHash carries the sha256 of the previous event's line bytes
// on the sink stream (raw line without the trailing newline).
// Together with the per-emit tail-hash update this forms a hash
// chain over the stream — any post-hoc alteration to a prior
// event breaks the chain at that point.
//
// The very first event of the AuditLogger's lifetime carries
// AuditChainGenesis ("00…00" — 32 zero bytes hex-encoded); verifiers
// treat that value as "no predecessor" and start their walk there.
// The field is written on EVERY event (no omitempty) — its absence
// is itself a tampering signal.
//
// PrevHash is covered by the Ed25519 signature (Sig) because the
// signature is computed after PrevHash is stamped and with only
// Sig blanked — so tampering with prev_hash breaks both the chain
// verification AND the signature verification. See #212 (R5) and
// #213 (R6) / docs/security/audit-tamper-evidence.md.
PrevHash string `json:"prev_hash"`
// Sigp identifies the canonicalization scheme used to produce the
// bytes over which Sig is computed. Present iff Sig is present.
// Currently one value:
//
// "jcs-1" — RFC 8785 (JCS) applied to the event with `sig`
// removed. Numbers are ES6-formatted; object keys are
// UTF-16-lexicographic-sorted; strings use minimal
// RFC 8259 escaping; no whitespace.
//
// Marked on the wire so verifiers know exactly which
// canonicalization to apply, and so future schemes can be added
// without confusion. The signature covers Sigp itself
// (canonicalize is called after Sigp is stamped, with only Sig
// blanked), so a tamperer can't downgrade the scheme.
Sigp string `json:"sigp,omitempty"`
// Kid identifies the audit signing key used to produce Sig.
// Consumers cross-reference it against the JWKS served at
// /.well-known/forge-audit-keys (or an out-of-band published
// keyset) to fetch the matching pubkey. Present iff the Forge
// deployment has audit signing enabled — otherwise absent.
// See docs/security/audit-signing.md (#213 / governance R6).
Kid string `json:"kid,omitempty"`
// Sig is the base64-encoded Ed25519 signature over the canonical
// preimage of this event with Sig itself empty. The preimage
// canonicalization is identified by Sigp (currently "jcs-1" — RFC
// 8785 JCS). Using JCS lets non-Go verifiers compute the preimage
// with any spec-compliant library, avoiding the "reproduce Go's
// encoding/json quirks" burden. It also sidesteps the large-int
// precision hole where json.Marshal(json.Unmarshal(x)) isn't a
// fixed point (JSON numbers decode to float64; JCS carries all
// numbers through the same ES6-double rule on both sides).
//
// The signature covers every other field including Sigp, Kid, and
// PrevHash — tampering with any of them (including the chain
// link or the canonicalization scheme) is detected at verify time.
//
// Present iff Kid is set. Absent (and never emitted) when
// audit signing is off. See #213 / governance R6.
Sig string `json:"sig,omitempty"`
// CorrelationID groups events from a single agent invocation —
// generated by the A2A handler at request entry.
CorrelationID string `json:"correlation_id,omitempty"`
// TaskID is the A2A task identifier (params.id on tasks/send).
TaskID string `json:"task_id,omitempty"`
// WorkflowID identifies the workflow DEFINITION that invoked this
// agent. Stable across every run of the same workflow. Sourced
// from X-Workflow-ID at request entry; absent for direct A2A
// invocations. SIEM consumers join on this for definition-level
// rollups ("top failing workflows"). FORGE-2 / issue #185 split.
WorkflowID string `json:"workflow_id,omitempty"`
// WorkflowExecutionID identifies the per-run instance of the
// workflow that invoked this agent. Unique per workflow
// execution. Sourced from X-Workflow-Execution-ID at request
// entry; absent for direct A2A invocations. SIEM consumers join
// on this for per-run timelines ("every event in this specific
// run, across every agent the orchestrator dispatched to"). Added
// in FORGE-2 / issue #185.
WorkflowExecutionID string `json:"workflow_execution_id,omitempty"`
// StageID identifies the workflow stage that invoked this agent.
StageID string `json:"stage_id,omitempty"`
// StepID identifies the workflow step that invoked this agent.
StepID string `json:"step_id,omitempty"`
// InvocationCaller identifies the upstream caller (orchestrator
// or upstream agent in an agent-to-agent flow).
InvocationCaller string `json:"invocation_caller,omitempty"`
// OrgID + WorkspaceID stamp the tenancy this agent run belongs
// to. Sourced from one of three layers (highest precedence first):
//
// 1. Explicit value set on the event before emit.
// 2. Per-request override headers parsed at the A2A boundary
// (X-Forge-Org-ID / X-Forge-Workspace-ID) and stashed on the
// context via WithTenancyContext.
// 3. Deployment-time stamp installed on the AuditLogger via
// WithTenancy(orgID, workspaceID) — typically populated from
// FORGE_ORG_ID / FORGE_WORKSPACE_ID at agent startup.
//
// Both keys use omitempty so deployments that don't set tenancy
// keep emitting the pre-tenancy JSON shape verbatim. The
// AuditSchemaVersion is NOT bumped — additive optional fields are
// schema-compatible per the documented policy. See issue #157.
//
// Distinct from the auth-derived `auth_verify.fields.org_id`,
// which continues to carry whatever the inbound token claimed.
// The top-level OrgID here is the operator's declared tenancy,
// trusted because the deployment / orchestrator set it.
OrgID string `json:"org_id,omitempty"`
WorkspaceID string `json:"workspace_id,omitempty"`
// EntityID + EntityType identify which entity emitted this event.
// Sourced from two layers (highest precedence first):
//
// 1. Explicit value set on the event before emit.
// 2. Deployment-time stamp installed on the AuditLogger via
// WithEntity(entityType, entityID) — typically populated from
// FORGE_AGENT_ID / cfg.AgentID at agent startup, with
// EntityType hardcoded to "agent" for now.
//
// No per-request ctx layer: entity identity is fixed at process
// startup. If an agent serves multiple tenancies per request, the
// OrgID / WorkspaceID layer above already covers that.
//
// Field names + values match the guardrails library's BasePayload
// vocabulary (EntityID, EntityType — "agent" / "workflow" /
// "assistant") so the Forge NDJSON stream lines up with the
// library's own vocabulary without a translation table. EntityType
// is hardcoded to "agent" today since Forge only runs agents;
// future entity types are an additive value change, not a schema
// change.
//
// Both keys use omitempty so deployments that don't set agent_id
// keep emitting the pre-#164 JSON shape verbatim.
EntityID string `json:"entity_id,omitempty"`
EntityType string `json:"entity_type,omitempty"`
// LLM call attribution (llm_call, llm_call_cancelled, invocation_complete).
Model string `json:"model,omitempty"`
Provider string `json:"provider,omitempty"`
// Token counts captured from provider response metadata. Nil when
// the event is not an LLM call. Non-nil with zero values + a true
// TokensUnavailable flag when the provider did not return usage
// (e.g. some self-hosted Ollama setups).
InputTokens *int `json:"input_tokens,omitempty"`
OutputTokens *int `json:"output_tokens,omitempty"`
// DurationMs is the wall-clock duration in milliseconds. Populated on
// llm_call, tool_exec, and invocation_complete events.
DurationMs *int64 `json:"duration_ms,omitempty"`
// RequestID is the provider-specific call identifier (Anthropic
// `id`, OpenAI `id`, etc.) — kept as an opaque debug-correlation
// handle, never used for cost attribution.
RequestID string `json:"request_id,omitempty"`
// TraceID + SpanID cross-link this audit event to the OTel trace
// the same logical operation produced (Phase 4 of the OTel
// Tracing v1 initiative — issue #105 / #108). Populated by
// EmitFromContext when the context carries a recording span;
// omitted when there is no span on the context or the tracer is
// the noop default (tracing disabled).
//
// Format: lowercase hex, matching W3C traceparent semantics —
// trace_id is 32 hex chars (128-bit), span_id is 16 hex chars
// (64-bit). Operators paste these directly into their trace
// backend's search box to pivot from an audit row to the parent
// trace, and vice versa.
//
// Backward compatibility: both fields use omitempty so consumers
// that have not been upgraded continue to see the pre-Phase-4
// shape verbatim (no trace_id / span_id keys at all). The
// AuditSchemaVersion is NOT bumped — adding optional fields is a
// schema-compatible change per the documented policy.
TraceID string `json:"trace_id,omitempty"`
SpanID string `json:"span_id,omitempty"`
Fields map[string]any `json:"fields,omitempty"`
}
AuditEvent is a single structured audit record emitted as NDJSON.
Workflow correlation fields (WorkflowID, WorkflowExecutionID, StageID, StepID, InvocationCaller) are tagged onto every event emitted via EmitFromContext when the request carries `X-Workflow-*` / `X-Invocation-Caller` headers from any A2A-compatible orchestrator. Direct A2A invocations omit them entirely so the JSON shape matches the pre-FWS-2 audit consumers.
WorkflowID / WorkflowExecutionID split (FORGE-2 / issue #185): `workflow_id` carries the workflow DEFINITION id (stable across all runs), `workflow_execution_id` carries the PER-RUN instance id. Audit consumers join on `workflow_execution_id` for per-run timelines and on `workflow_id` for definition-level rollups.
Token usage, duration, model, and provider fields (issue #87 / FWS-3) are populated by the LLM call site, tool execution path, and per- invocation lifecycle. They use *int / *int64 pointers so the JSON distinguishes "field absent" (nil) from "field present with zero value" — important for llm_call events where zero is a legitimate count and TokensUnavailable signals "provider did not report usage."
Field naming aligns with OTel GenAI semconv (input_tokens / output_tokens / duration_ms) so audit consumers can correlate Forge audit events with OTel traces without a translation table.
type AuditExportConfig ¶
type AuditExportConfig struct {
// SocketPath is the absolute path to the in-pod Unix Domain Socket
// the sidecar listens on. Empty disables the socket sink.
SocketPath string
// HTTPEndpoint is a localhost URL (e.g. "http://127.0.0.1:9097/v1/audit")
// the fallback HTTP sink POSTs to. Empty disables the HTTP sink.
// Ignored when SocketPath is set.
HTTPEndpoint string
// WriteTimeout bounds each per-event sink write. Default 50ms.
// Applies to both the socket and HTTP sinks. The stderr safety-net
// sink ignores this — stderr writes are bounded by the kernel's
// pipe buffer, not by us.
WriteTimeout time.Duration
// DialTimeout bounds the initial socket dial. Default 1s. Ignored
// by the HTTP sink (which sets its own per-request timeout to
// match WriteTimeout).
DialTimeout time.Duration
}
AuditExportConfig configures the FWS-7 export sinks (issue #95). It is intentionally minimal — three knobs only — because each one maps to a single CLI flag / env var pair and corresponds to one operational decision the deployer has to make:
- SocketPath: "where does the in-pod sidecar listen?"
- HTTPEndpoint: "where does the fallback HTTP receiver listen?"
- WriteTimeout: "how long am I willing to spend per emit before dropping?"
Default zero value means "no export sinks; behave exactly like pre-FWS-7 (stderr only)." This is the right default because the initializ platform deploy receiver injects the env vars; self-managed deployments without a sidecar get the unchanged stderr stream.
When both SocketPath and HTTPEndpoint are non-empty, SocketPath wins (preferred sink path). The HTTP fallback is purely for environments where Unix sockets aren't available — typically Windows containers or platform-managed sandboxes that forbid unix:// dialing.
func AuditExportConfigFromEnv ¶
func AuditExportConfigFromEnv() AuditExportConfig
AuditExportConfigFromEnv reads the three env vars and returns a populated config. Designed for the case where the CLI flag was not set; the caller (`forge run --audit-socket=...`) overrides specific fields after this call. WriteTimeout parses Go duration syntax ("50ms", "200ms"); a parse failure falls back to default (zero, which downstream maps to 50ms).
type AuditLogger ¶
type AuditLogger struct {
// contains filtered or unexported fields
}
AuditLogger fans serialized NDJSON audit events out to a slice of Sinks. The traditional single-writer constructor wraps the writer in a writerSink; the FWS-7 multi-sink constructor (NewAuditLoggerFromConfig) composes a stderr safety-net sink with an optional Unix socket or localhost HTTP sink for export to a sidecar.
Emit-side semantics:
- Each sink's Write is called sequentially. Each sink is responsible for its own timeout/drop behavior; the AuditLogger never spawns a goroutine per event. This bounds emit latency to the sum of sink timeouts (stderr is microseconds; socket/HTTP is the configured per-write timeout, default 50ms).
- Errors from a sink are logged once per (sink, error-class) and suppressed thereafter — a broken sidecar must not flood the operational logs.
- Events leaving each sink are byte-identical. No sink transforms the payload.
func NewAuditLogger ¶
func NewAuditLogger(w io.Writer) *AuditLogger
NewAuditLogger creates a single-sink AuditLogger wrapping the given writer. Backward-compatible with pre-FWS-7 callers; tests and the CLI's per-command audit loggers (channel.go / run.go) continue to use this. Production code paths that need the export sink should use NewAuditLoggerFromConfig.
func NewAuditLoggerFromConfig ¶
func NewAuditLoggerFromConfig(cfg AuditExportConfig) *AuditLogger
NewAuditLoggerFromConfig constructs an AuditLogger with the standard FWS-7 sink stack:
- stderr safety-net sink (always registered first; the operator can still grep audit NDJSON out of container logs even if the sidecar is down)
- socket sink when cfg.SocketPath is set
- HTTP sink when cfg.SocketPath is empty and cfg.HTTPEndpoint is set
When both export-sink fields are empty, behavior is identical to NewAuditLogger(os.Stderr) — pre-FWS-7 compatibility.
func (*AuditLogger) AddSink ¶
func (a *AuditLogger) AddSink(s Sink)
AddSink appends a sink to the fan-out. Safe to call after construction (e.g. from a delayed sidecar discovery), but most callers should construct via NewAuditLoggerFromConfig.
func (*AuditLogger) Close ¶
func (a *AuditLogger) Close(ctx context.Context) error
Close drains every sink with the given deadline. Honors the context; sinks that don't drain in time are abandoned (each sink's Close is responsible for its own per-sink deadline derivation from ctx). Returns the first non-nil error from any sink so callers can surface shutdown problems; later errors are still logged via opsLog.
func (*AuditLogger) Emit ¶
func (a *AuditLogger) Emit(event AuditEvent)
Emit serializes an event and fans it out to every registered sink. Timestamp is populated to RFC3339 (UTC) if absent. Marshal failures are silently dropped — they indicate a programmer error (an AuditEvent with a non-serializable Fields value), and dropping matches the pre-FWS-7 behavior. Per-sink errors are logged once.
Callers that have a request context.Context in scope should prefer EmitFromContext, which auto-tags CorrelationID, TaskID, and workflow-correlation fields.
func (*AuditLogger) EmitChannelDeniedByPolicy ¶
func (a *AuditLogger) EmitChannelDeniedByPolicy(channel, layer, source string)
EmitChannelDeniedByPolicy records that a channel adapter was skipped at startup because a policy layer's denied_channels list named it. `layer` identifies which file enforced ("system" / "user" / "workspace"); `source` is that file's path. Unlike egress/tool/model violations, channel deny does NOT abort startup — the agent runs without the denied channel. Operators see the skip in their audit pipeline and group by layer to understand which policy file owns the decision. See issue #90 / FWS-6.
func (*AuditLogger) EmitFromContext ¶
func (a *AuditLogger) EmitFromContext(ctx context.Context, event AuditEvent)
EmitFromContext writes an audit event after auto-tagging CorrelationID, TaskID, and workflow-correlation fields from the request context. Fields already set on the passed event are preserved — the context is a fallback, not an override. This makes it safe to migrate callers from Emit to EmitFromContext: any already-explicit value continues to win.
func (*AuditLogger) EmitInvocationCancelled ¶
func (a *AuditLogger) EmitInvocationCancelled(ctx context.Context, reason CancellationReason, duration time.Duration, fields map[string]any)
EmitInvocationCancelled emits an invocation_cancelled audit event for an in-flight A2A invocation that was signalled mid-execution via tasks/cancel (or internal cancellation: parent ctx deadline, graceful shutdown). Routed through EmitFromContext so workflow correlation auto-tags. The reason is folded into Fields["reason"] as a string — operators classify these via the CancellationReason constants but consumers should pass-through unknown values.
Partial usage data should be present in fields (the runner reads the per-invocation LLMUsageAccumulator snapshot and adds input_tokens_total / output_tokens_total / llm_call_count / model / provider when llm_call_count > 0). When no LLM calls completed before cancellation, the field map carries reason + state only — downstream billing sees zero tokens which is correct: the invocation was cancelled before incurring spend.
See issue #88 / FWS-4.
func (*AuditLogger) EmitInvocationComplete ¶
func (a *AuditLogger) EmitInvocationComplete(ctx context.Context, duration time.Duration, fields map[string]any)
EmitInvocationComplete emits an invocation_complete audit event carrying the total wall-clock duration of the A2A invocation (auth → dispatch → engine.Execute → response). Routed through EmitFromContext so workflow-correlation fields are inherited from the inbound request. One event per invocation; emitted by the runner at the response boundary. See issue #87 / FWS-3.
func (*AuditLogger) EmitLLMCall ¶
func (a *AuditLogger) EmitLLMCall(ctx context.Context, args LLMCallAuditArgs)
EmitLLMCall builds and emits an llm_call (or llm_call_cancelled) audit event from the captured args. Routed through EmitFromContext so workflow-correlation fields (workflow_id / stage_id / step_id / invocation_caller from FWS-2) auto-tag every LLM call event when the inbound request carried orchestrator headers. This is the shared capture point that the OTel tracing work will hook into. See issue #87 / FWS-3.
func (*AuditLogger) EmitPolicyLoaded ¶
func (a *AuditLogger) EmitPolicyLoaded(fields map[string]any)
EmitPolicyLoaded emits a policy_loaded audit event at agent startup when a non-zero platform policy is active. Fields are a summary of the effective policy (deny-list sizes, max bounds, source path) — NOT the full policy contents, which can be large and may contain internal infrastructure hints operators don't want in every audit stream. Consumers that need the full policy can read the source file via the path field.
Emitted via plain Emit (not EmitFromContext) because no request context exists at startup. See issue #89 / FWS-5.
func (*AuditLogger) EmitPolicyViolationAtBuildTime ¶
func (a *AuditLogger) EmitPolicyViolationAtBuildTime(fields map[string]any)
EmitPolicyViolationAtBuildTime emits a policy_violation_at_build_time audit event when forge.yaml's declaration conflicts with the platform policy. Fields carry the conflict detail (which kind of violation — denied_egress, denied_tool, forbidden_model, size_bound — and the offending value(s)). Called once at startup before the runner returns a non-zero exit; the audit lands even though the agent never serves traffic, so the operator's audit pipeline captures the violation.
Emitted via plain Emit (not EmitFromContext) because no request context exists at startup. See issue #89 / FWS-5.
func (*AuditLogger) EmitToolExec ¶
func (a *AuditLogger) EmitToolExec(ctx context.Context, tool string, duration time.Duration, fields map[string]any)
EmitToolExec emits a tool_exec audit event tagged with the tool name + wall-clock duration. Routed through EmitFromContext so workflow-correlation fields auto-tag every tool execution when the inbound request was orchestrated. The Fields map may carry arg-shape metadata (e.g. arg sizes, types) — raw arg values are deliberately not emitted here; that question is FWS-8's payload-stripping concern, not FWS-3's. See issue #87 / FWS-3.
func (*AuditLogger) SetOpsLogger ¶
func (a *AuditLogger) SetOpsLogger(l Logger)
SetOpsLogger wires a structured logger into the audit pipeline for reporting sink failures (one log per (sink, error-class)). nil disables ops logging; in that mode sink errors are silently swallowed — appropriate for tests and for the channel CLI subcommand where there's no logger in scope.
func (*AuditLogger) SetSigner ¶ added in v0.17.0
func (a *AuditLogger) SetSigner(s *AuditSigner)
SetSigner installs an Ed25519 signer so every subsequent event is signed. Passing nil disables signing (subsequent events emit without Sig / Kid). Signing is opt-in — the runner calls this at startup when audit signing is configured; no configuration means pre-#213 wire shape. See docs/security/audit-signing.md.
func (*AuditLogger) Sinks ¶
func (a *AuditLogger) Sinks() []Sink
Sinks returns a snapshot of currently registered sinks. Used by the periodic audit_export_status emitter to read per-sink stats.
func (*AuditLogger) WithEntity ¶
func (a *AuditLogger) WithEntity(entityType, entityID string) *AuditLogger
WithEntity installs the deployment-time entity stamp on the AuditLogger. entityType matches the guardrails library's EntityType constants ("agent" / "workflow" / "assistant"); today Forge only runs agents, so the runner hardcodes "agent". Empty arguments disable the stamp for that field. Called once at runner startup after resolving FORGE_AGENT_ID / cfg.AgentID. Returns the receiver for fluent construction.
Precedence at emit time (highest first):
- Explicit EntityID/EntityType set on the AuditEvent.
- The static stamp installed here.
No per-request context layer: entity identity is fixed at process startup. If a deployment needs per-request entity routing, that's the tenancy layer's job (OrgID/WorkspaceID) — agent identity is the process, by definition.
See issue #164.
func (*AuditLogger) WithTenancy ¶
func (a *AuditLogger) WithTenancy(orgID, workspaceID string) *AuditLogger
WithTenancy installs the deployment-time tenancy stamp on the AuditLogger. Both arguments are optional — passing "" disables the stamp for that field. Called once at runner startup after resolving FORGE_ORG_ID / FORGE_WORKSPACE_ID. Returns the receiver for fluent construction.
Precedence at emit time (highest first):
- Explicit OrgID/WorkspaceID set on the AuditEvent.
- TenancyContext from the request context (per-request override header X-Forge-Org-ID / X-Forge-Workspace-ID).
- The static stamp installed here.
Setting tenancy on an already-running AuditLogger is allowed but not the common path; hot-reload is the typical caller.
type AuditPayloadCapture ¶
type AuditPayloadCapture struct {
// LLMMessages controls whether each `llm_call` event carries the
// list of inbound chat messages (role + content) the agent sent
// to the model. Off by default.
LLMMessages bool
// LLMResponse controls whether `llm_call` carries the model's
// completion text. Off by default.
LLMResponse bool
// ToolArgs controls whether `tool_exec` carries the raw input
// the agent passed to the tool. Off by default.
ToolArgs bool
// ToolResult controls whether `tool_exec` carries the raw
// output the tool returned. Off by default.
ToolResult bool
// Redact runs the vendor-secret regex scrub
// (content_redact.go:RedactSecrets) on each captured field
// before truncation. The struct's zero value has Redact=false
// purely as a Go-zero-value artifact — operators MUST go through
// AuditPayloadCaptureFromEnv (which defaults Redact=true) or
// explicitly set the field. The runner's resolver enforces this
// by always passing through FromEnv before applying any
// forge.yaml overrides.
//
// Disabling redact is a deliberate escape hatch: operators whose
// downstream sink has its own scrubber and who explicitly want to
// see the raw bytes for tuning. See issue #163.
Redact bool
// CapLLMMessagesBytes is the max bytes serialized for the
// captured chat messages array. 0 = use the package default
// (DefaultPayloadCaptureCapBytes).
CapLLMMessagesBytes int
// CapLLMResponseBytes — same shape, for completion text.
CapLLMResponseBytes int
// CapToolArgsBytes — same shape, for tool input.
CapToolArgsBytes int
// CapToolResultBytes — same shape, for tool output.
CapToolResultBytes int
}
AuditPayloadCapture controls whether the audit pipeline emits raw LLM prompt / completion text and raw tool args / results in audit events. Every capture flag defaults to false; the default audit posture is "metadata only" (size + type + token counts + duration). This matches the security commitment baked into the audit emission sites today and codified by FWS-8 (issue #91).
Customers who need raw payloads in audit (debugging, replay, supervised-learning corpora) opt in field by field, NEVER globally. The cap fields bound per-event byte size so a 1MB prompt doesn't turn one audit event into a memory-hostile record — the captured substring is the first CapXxxBytes bytes followed by a truncation marker `…[truncated:N]`.
Capture flags + caps are read by the runner's hook-registered audit emitters (registerAuditHooks). The Sink layer is unaware of capture settings — it just emits what the AuditEvent says.
THIS IS A SECURITY-RELEVANT CONFIGURATION. Operators who enable any capture flag should keep Redact on (the default) so vendor secrets that an LLM glues into a `cli_execute` command (a real failure mode — the model remembers a token from the prompt and inlines it into a shell invocation) are scrubbed before the event lands on the audit stream. Set Redact=false ONLY when a downstream sink runs its own scrubber. See PrepareCapturedContent in content_redact.go.
Beyond redact: operators must also ensure the audit transport (the FWS-7 sink or the stderr safety net) lands in a store that respects the captured payloads' sensitivity (PII, prompts, etc.).
func AuditPayloadCaptureFromEnv ¶ added in v0.16.0
func AuditPayloadCaptureFromEnv() AuditPayloadCapture
AuditPayloadCaptureFromEnv reads the FORGE_AUDIT_CAPTURE_* env vars and returns a populated config. Redact defaults to TRUE so flipping any capture flag on without touching Redact keeps the safer posture — the same default the GuardrailAuditConfig.Redact uses for guardrail evidence and the OTel tracing CaptureContent path uses for span content. See issue #163 for the consolidation rationale.
FORGE_AUDIT_CAPTURE_MAX_BYTES is the single-knob per-field cap: when set it populates all four CapXxxBytes fields uniformly. Operators who need different caps per field set the struct directly (Forge embedded as a library) — env-var coverage is intentionally single-knob to keep the operator surface small.
Parse failures (non-boolean strings for the flags, non-integer for the cap) fall through to defaults. Same forgiving posture as the other Audit*FromEnv constructors.
func (AuditPayloadCapture) AnyEnabled ¶
func (c AuditPayloadCapture) AnyEnabled() bool
AnyEnabled reports whether at least one capture flag is on. The runner skips the hook overhead entirely when nothing is enabled.
type AuditSigner ¶ added in v0.17.0
type AuditSigner struct {
// contains filtered or unexported fields
}
AuditSigner mints Ed25519 signatures over the canonical JSON of an AuditEvent. Nil signer is a valid state — signing is opt-in (an operator who hasn't wired a key gets the pre-#213 wire shape).
Loading key material is separated from signing so the same key source can be used for the JWKS endpoint's public-key advertise path. See forge-cli/runtime/runner.go for the wiring.
func NewAuditSigner ¶ added in v0.17.0
func NewAuditSigner(k LoadedKey) *AuditSigner
NewAuditSigner constructs a signer around a loaded key.
func (*AuditSigner) Kid ¶ added in v0.17.0
func (s *AuditSigner) Kid() string
Kid returns the current key id — surfaced on every signed event and on the JWKS pubkey record.
type CancellationReason ¶
type CancellationReason string
CancellationReason classifies why an in-flight A2A invocation was cancelled. Sourced from the tasks/cancel JSON-RPC params and carried onto the invocation_cancelled audit event so a downstream consumer (cost aggregator, workflow UI, SIEM) can distinguish "the operator hit stop" from "the orchestrator hit a cost ceiling." See issue #88 / FWS-4.
New reasons are additive — audit consumers that don't recognize a reason string should pass it through, not reject the event.
const ( // CancelReasonWorkflowFailure is set by the orchestrator when a // sibling step in a parallel stage failed under fail_workflow // semantics and this in-flight agent should abandon its work. CancelReasonWorkflowFailure CancellationReason = "workflow_failure" // CancelReasonCostLimitExceeded is set by the orchestrator when // the workflow's cumulative cost ceiling (from the FWS-3 token // totals in A2A response headers) was hit and the platform is // cutting off further LLM spend. CancelReasonCostLimitExceeded CancellationReason = "cost_limit_exceeded" // CancelReasonTimeout is set by the orchestrator (or by Forge's // own task deadline) when the wall-clock budget for the // invocation has been exhausted. CancelReasonTimeout CancellationReason = "timeout" // CancelReasonExternalSignal is the default — operator-initiated // cancel, debugging stop, anything else not covered by the more // specific reasons. CancelReasonExternalSignal CancellationReason = "external_signal" )
func CancellationReasonFromCause ¶
func CancellationReasonFromCause(ctx context.Context) CancellationReason
CancellationReasonFromCause unwraps the reason stamped on ctx by the tasks/cancel path. Call this after observing ctx.Err() in the executeTask goroutine. Returns CancelReasonExternalSignal when ctx was cancelled without a typed reason (e.g. parent ctx deadline, graceful shutdown signal) — those are still cancellations but the emitting handler didn't classify them.
func (CancellationReason) IsValid ¶
func (r CancellationReason) IsValid() bool
IsValid reports whether r is one of the documented reason values. Used at the tasks/cancel boundary to validate operator input; the runtime itself happily forwards whatever string was supplied — the validation is a UX nicety, not a security boundary.
type CancellationRegistry ¶
type CancellationRegistry struct {
// contains filtered or unexported fields
}
CancellationRegistry tracks in-flight A2A invocations so the tasks/cancel handler can signal them. One registry per Runner; one entry per active invocation, keyed by task ID.
The registry is the bridge between the JSON-RPC handler (which sees the cancel request) and the long-running executeTask goroutine (which holds the context.CancelCauseFunc). The handler looks up the task ID, invokes the stored cancel function with a typed reason, and the goroutine's ctx propagates the cancellation through the LLM client, tool execution, and audit emission.
func NewCancellationRegistry ¶
func NewCancellationRegistry() *CancellationRegistry
NewCancellationRegistry returns a fresh empty registry.
func (*CancellationRegistry) Cancel ¶
func (r *CancellationRegistry) Cancel(taskID string, reason CancellationReason) bool
Cancel signals the in-flight invocation for taskID with a typed reason. Returns true when an entry was found and its cancel function invoked, false when no invocation is registered (already completed, never started, or already cancelled and unregistered). The handler maps false → a no-op response so cancel-after-complete is idempotent rather than an error.
Reason validation is the caller's job; Cancel forwards whatever it gets so internal cancellations (graceful shutdown, parent deadline translation) can supply their own reason without going through the JSON-RPC validator.
func (*CancellationRegistry) Len ¶
func (r *CancellationRegistry) Len() int
Len returns the number of in-flight registrations. Exposed for tests and operational observability — there is no per-task lookup API by design (the handler only needs Cancel; the executeTask goroutine reads its own reason via context.Cause on ctx).
func (*CancellationRegistry) Register ¶
func (r *CancellationRegistry) Register(taskID string, cancel context.CancelCauseFunc) (release func())
Register associates a CancelCauseFunc with a task ID. Returns a release closure the caller must defer — release pops the entry from the registry so it doesn't leak after the invocation finishes (success, failure, or cancellation).
If a registration already exists for taskID (concurrent retries on the same ID, or buggy callers), Register overwrites it. The returned release uses pointer identity to pop only its own entry, so a stale release from the previous owner is a no-op.
type Compactor ¶
type Compactor struct {
// contains filtered or unexported fields
}
Compactor manages memory compaction by summarizing old messages and optionally flushing to disk.
Each Memory instance is single-threaded per task execution (the agent loop is sequential), so holding mem.mu during the LLM summarization call is acceptable — no concurrent access occurs.
func NewCompactor ¶
func NewCompactor(cfg CompactorConfig) *Compactor
NewCompactor creates a Compactor from the given config.
func (*Compactor) MaybeCompact ¶
MaybeCompact checks whether the memory exceeds the trigger threshold and, if so, compacts the oldest 50% of messages into a summary. Returns true if compaction occurred.
The first user message (the original task request) is always preserved so the LLM retains the objective across compaction cycles.
The method holds mem.mu for its entire duration including any LLM call. This is safe because each Memory is used by a single sequential agent loop.
func (*Compactor) SetMemoryFlusher ¶
func (c *Compactor) SetMemoryFlusher(f MemoryFlusher)
SetMemoryFlusher sets the long-term memory flusher. This allows wiring the flusher after construction (e.g., when the memory manager depends on the same embedder resolution that happens after the compactor is created).
type CompactorConfig ¶
type CompactorConfig struct {
// Client is the LLM client for abstractive summarization. If nil,
// only extractive (bullet-point) summarization is used.
Client llm.Client
// Store persists sessions to disk after compaction. If nil, compaction
// still reduces in-memory messages but nothing is flushed.
Store SessionStore
// Logger for compaction events.
Logger Logger
// CharBudget is the total character budget. Compaction triggers when
// totalChars exceeds CharBudget * TriggerRatio. Default: 200,000.
CharBudget int
// TriggerRatio is the fraction of CharBudget at which compaction fires.
// Default: 0.6.
TriggerRatio float64
// MemoryFlusher flushes key observations to long-term memory before
// compaction discards old messages. Optional.
MemoryFlusher MemoryFlusher
}
CompactorConfig configures a Compactor.
type Decision ¶ added in v0.16.0
type Decision struct {
// Allowed reports whether the invocation proceeds.
Allowed bool
// Reason is the platform's failure code on deny. Empty on admit.
// Stable enough for SIEM grouping (`cost_limit_exceeded`,
// `billing_overdue`, `rate_limit_exhausted`, …) — vocabulary
// owned by the platform.
Reason string
// Scope names which level in the platform's billing hierarchy
// tripped — `agent` / `workspace` / `org` / `""`. Purely
// informational for audit + SRE runbook routing.
Scope string
// Window names the quota window that tripped — `hourly`,
// `daily`, `monthly`, `billing_cycle`, … Platform-defined.
// Lets the audit answer "was this a daily cap or a per-minute
// burst?" without joining against platform state.
Window string
// ResetAt is when the deny clears (when the platform expects the
// caller could retry successfully). Zero when unknown. Drives
// the Retry-After header Forge stamps on the 402 response.
ResetAt time.Time
// Cached reports whether this decision came from Forge's local
// per-agent TTL cache rather than a fresh platform call.
// Reaches the audit event and span; helps operators debug
// propagation lag when the platform "should have" already
// re-admitted an agent.
Cached bool
// Fallback is true when Allowed=true was forced by a platform
// call failure (timeout, 5xx, network error). The Decision is
// indistinguishable from a real admit on the wire, but operators
// alerting on `forge.admission.fallback=true` see the platform
// outage rate even though no caller ever observed it as a deny.
Fallback bool
}
Decision is the result of an admission check. Allowed=true means the invocation proceeds. Allowed=false means the middleware short-circuits with HTTP 402 and the other fields shape the response body + audit event + span attributes.
Every non-Allowed field is platform-defined and opaque to Forge — they ride verbatim through the audit/span/response surface. The platform owns the vocabulary (e.g. `cost_limit_exceeded`, `billing_overdue`, `daily`, `monthly`). Forge never enums them.
Cached + Fallback record observability metadata about how Forge reached the decision, not the decision content itself. They are stamped on the audit event and the OTel span so operators can distinguish a fresh platform "deny" from a cached one or a fail- open admit driven by a platform outage.
type DeferSpec ¶ added in v0.17.0
type DeferSpec struct {
// To identifies the decision target. Free-form string so the
// runtime can route to the right channel/notifier — typical
// shapes:
// - "channel:slack:#oncall"
// - "channel:telegram:@sec-lead"
// - "human:user-42"
// - "external:https://approvals.internal/decisions"
// The defer engine relays this verbatim; interpretation lives
// in the notify adapter.
To string
// Timeout is the maximum wait before the deferral auto-DENYs.
// Zero → 10 minutes (a sensible ceiling; sync callers holding
// HTTP connections open longer than that risk proxy timeouts).
Timeout time.Duration
// ContextForApprover is a short (< 4KB) human-readable payload
// the approver sees to make the decision. Should NOT contain
// secrets or raw PII — the audit stream captures a truncated
// copy on the `task_deferred` event. Include: the tool name,
// the exact args the LLM chose, why the guardrail thought this
// needed a human decision.
ContextForApprover string
}
DeferSpec describes an R4c deferred decision — the target that will decide (typically a human on a channel), the maximum wait before auto-DENY, and the context payload the approver sees.
type FallbackModelConfig ¶
type FallbackModelConfig struct {
Provider string
Client llm.ClientConfig
}
FallbackModelConfig holds a resolved fallback provider's configuration.
type GuardrailChecker ¶
type GuardrailChecker interface {
// CheckInbound validates an inbound (user) message — InputGate.
// Returns a PolicyResult carrying the engine's decision. On
// DecisionModify the implementation SHOULD have already mutated
// msg in place so the caller's downstream reads see the redacted
// content; PolicyResult.Modified is provided for audit-trail use.
// On DecisionDeny the caller MUST NOT admit the message.
CheckInbound(ctx context.Context, msg *a2a.Message) (PolicyResult, error)
// CheckOutbound validates an outbound (agent) message —
// OutputGate. Same semantics as CheckInbound: MODIFY mutates
// msg in place, DENY blocks.
CheckOutbound(ctx context.Context, msg *a2a.Message) (PolicyResult, error)
// CheckToolCall validates the arguments the agent is about to
// pass to a tool — ToolCallGate. Called from the BeforeToolExec
// hook. Returns the (possibly redacted) args string and any
// blocking error. Empty args short-circuit to (args, nil).
CheckToolCall(ctx context.Context, toolName, args string) (string, error)
// CheckToolOutput scans tool output text — OutputGate with tool
// metadata so the emitted guardrail_check carries `tool` for
// SIEM grouping. Returns the (possibly redacted) text.
CheckToolOutput(ctx context.Context, toolName, text string) (string, error)
// CheckContext validates retrieved context (RAG chunks, memory
// recall, dynamic system-prompt content) before it is injected
// into the LLM prompt — ContextGate. Returns the (possibly
// redacted) content. Empty content short-circuits.
//
// The current Forge call site is the BeforeLLMCall hook, which
// scans system-role messages assembled by the loop. Future memory
// / RAG work can call this directly from the recall path when a
// dedicated context-injection seam exists.
CheckContext(ctx context.Context, content string) (string, error)
// CheckStream validates a single chunk emitted by a streaming
// LLM call — StreamGate. Returns the (possibly redacted) chunk.
//
// Forge's current Execute loop does not call provider streaming
// (ExecuteStream is a buffered wrapper around non-streaming
// Execute), so this is not auto-wired yet. The method is exposed
// for callers that consume llm.Client.ChatStream directly and
// for future loop work that adds a real per-chunk seam.
CheckStream(ctx context.Context, chunk string) (string, error)
}
GuardrailChecker validates messages, tool calls, retrieved context, and tool / LLM output against guardrail policies. Implementations may use file-based config, database-backed config, or no-op passthrough.
Method names mirror the five gates the underlying guardrails library distinguishes (input / context / tool_call / output / stream) rather than the older inbound/outbound nomenclature. See issue #159.
All Check methods accept a context so implementations can route audit emissions through AuditLogger.EmitFromContext and inherit correlation_id, task_id, sequence number, tenancy, and workflow tags from the request scope.
type Hook ¶
type Hook func(ctx context.Context, hctx *HookContext) error
Hook is a function invoked at a specific point in the agent loop.
type HookContext ¶
type HookContext struct {
Messages []llm.ChatMessage
Response *llm.ChatResponse
ToolName string
ToolInput string
ToolOutput string
Error error
TaskID string
CorrelationID string
// LLMCallDuration is the wall-clock time spent in the provider
// client.Chat call. Populated for AfterLLMCall hooks.
LLMCallDuration time.Duration
// Provider / Model identify the LLM provider + model used for the
// call. Populated for AfterLLMCall hooks so audit + A2A-header
// emitters can stamp attribution without re-walking config.
Provider string
Model string
// ToolExecDuration is the wall-clock time spent executing the tool.
// Populated for AfterToolExec hooks.
ToolExecDuration time.Duration
}
HookContext carries data available to hooks at each hook point.
LLMCallDuration / ToolExecDuration / Provider / Model are populated at the call site (loop.go) before the After* hook fires, so audit emitters can tag llm_call and tool_exec events with wall-clock timing and provider attribution. See issue #87 / FWS-3.
type HookRegistry ¶
type HookRegistry struct {
// contains filtered or unexported fields
}
HookRegistry manages registered hooks for each hook point.
func NewHookRegistry ¶
func NewHookRegistry() *HookRegistry
NewHookRegistry creates an empty HookRegistry.
func (*HookRegistry) Fire ¶
func (r *HookRegistry) Fire(ctx context.Context, point HookPoint, hctx *HookContext) error
Fire invokes all hooks registered for the given point in order. If any hook returns an error, execution stops and the error is returned.
func (*HookRegistry) Register ¶
func (r *HookRegistry) Register(point HookPoint, h Hook)
Register adds a hook for the given point. Hooks fire in registration order.
type JSONLogger ¶
type JSONLogger struct {
// contains filtered or unexported fields
}
JSONLogger writes structured JSON log entries to an io.Writer.
func NewJSONLogger ¶
func NewJSONLogger(w io.Writer, verbose bool) *JSONLogger
NewJSONLogger creates a JSONLogger writing to w. Debug entries are only emitted when verbose is true.
type JWK ¶ added in v0.17.0
type JWK struct {
Kty string `json:"kty"` // "OKP"
Crv string `json:"crv"` // "Ed25519"
X string `json:"x"` // base64url of pubkey (no padding)
Kid string `json:"kid,omitempty"` // operator-supplied key id
Use string `json:"use,omitempty"` // "sig"
Alg string `json:"alg,omitempty"` // "EdDSA"
}
JWK is one entry — Ed25519 pubkey per RFC 8037.
type JWKS ¶ added in v0.17.0
type JWKS struct {
Keys []JWK `json:"keys"`
}
JWKS is the JSON Web Key Set shape served at /.well-known/forge-audit-keys. Consumers pull it once at startup and cache locally; rotation adds a new entry alongside the old.
func PublicJWKS ¶ added in v0.17.0
PublicJWKS produces the JWKS representation of the loaded keys. A nil / empty input returns an empty set — safe to serve when signing is off.
type LLMCallAuditArgs ¶
type LLMCallAuditArgs struct {
Model string
Provider string
RequestID string
Usage LLMUsage
Duration time.Duration
// Cancelled flips the emitted event from llm_call to llm_call_cancelled.
// Used for streaming calls aborted mid-flight; partial usage counts are
// still carried.
Cancelled bool
// Failed flips the emitted event to llm_call_failed (#361): the provider
// or gateway errored/rejected the call. ErrorText carries the bounded
// error detail (e.g. an input_schema validation message) into
// fields.error so the failure reason reaches the audit stream, not just
// pod logs. Failed takes precedence over Cancelled.
//
// Privacy: fields.error is NOT subject to the payload-capture toggle
// (an operator who disabled capture still needs failure reasons), so it
// is ALWAYS secret-scrubbed (RedactSecrets) and capped at 512B — a
// provider that echoes a request fragment in an error body can't leak a
// credential into the stream (review #362).
Failed bool
ErrorText string
// Fields carries optional extra metadata to fold into the emitted
// event's `fields` map. Populated by the runner's hook layer when
// AuditPayloadCapture has any flag enabled (issue #91 / FWS-8):
// captured prompt_messages, completion_text, etc. Nil for the
// default metadata-only audit posture.
Fields map[string]any
}
LLMCallAuditArgs is the shared input to AuditLogger.EmitLLMCall. The LLM call site captures these fields once at provider-call completion and the audit logger fans them out to the llm_call NDJSON event. The OTel tracing work (FORGE_OTEL_TRACING.md) will hook into this same capture point to populate gen_ai.usage.input_tokens / gen_ai.usage.output_tokens span attributes without re-doing the per-provider extraction. See issue #87 / FWS-3.
type LLMExecutor ¶
type LLMExecutor struct {
// contains filtered or unexported fields
}
LLMExecutor implements AgentExecutor using an LLM client with tool calling.
func NewLLMExecutor ¶
func NewLLMExecutor(cfg LLMExecutorConfig) *LLMExecutor
NewLLMExecutor creates a new LLMExecutor with the given configuration.
func (*LLMExecutor) Execute ¶
func (e *LLMExecutor) Execute(ctx context.Context, task *a2a.Task, msg *a2a.Message) (outMsg *a2a.Message, outErr error)
Execute processes a message through the LLM agent loop.
func (*LLMExecutor) ExecuteStream ¶
func (e *LLMExecutor) ExecuteStream(ctx context.Context, task *a2a.Task, msg *a2a.Message) (<-chan *a2a.Message, error)
ExecuteStream runs the tool-calling loop non-streaming, then emits the final response as a single message on the channel. True word-by-word streaming is v2.
type LLMExecutorConfig ¶
type LLMExecutorConfig struct {
Client llm.Client
Tools ToolExecutor
Hooks *HookRegistry
SystemPrompt string
MaxIterations int
Compactor *Compactor
Store SessionStore
Logger Logger
ModelName string // model name for context-aware budgeting
Provider string // provider name (anthropic, openai, ollama, custom) — for audit attribution
CharBudget int // explicit char budget override (0 = auto from model)
FilesDir string // directory for file_create output (default: $TMPDIR/forge-files)
SessionMaxAge time.Duration // max idle time before session recovery is skipped (0 = 30m default)
WorkflowPhases []string // workflow phases from skills (edit, finalize, query)
// DeferToolResultTruncation applies the tool-result size cap after the
// AfterToolExec hooks instead of before, behind a pre-hook safety
// ceiling (16x the cap, absolute max 4MB). Enable when a compression
// hook is registered: pre-hook truncation both destroys data and breaks
// the JSON envelopes the compressor could otherwise shrink losslessly.
DeferToolResultTruncation bool
// TracingConfig is the same observability.TracingConfig the cli
// runner resolves and passes to NewTracerProvider. The executor
// reads CaptureContent + Redact to decide whether to stamp
// prompt / completion / tool I/O content on Phase 3 spans
// (issue #130). Zero value disables content capture.
TracingConfig observability.TracingConfig
}
LLMExecutorConfig configures the LLM executor.
type LLMUsage ¶
LLMUsage carries the normalized token counts an LLM call site captures from provider response metadata. Mirrors llm.UsageInfo but kept in the runtime package so the audit layer has no llm-package dependency. The audit emitter sets TokensUnavailable=true on the event when both Input and Output are zero — signal to billing consumers that the provider did not report usage rather than "the call genuinely consumed zero tokens."
type LLMUsageAccumulator ¶
type LLMUsageAccumulator struct {
// contains filtered or unexported fields
}
LLMUsageAccumulator aggregates per-invocation LLM usage so the A2A response handler can populate X-Forge-Tokens-In / X-Forge-Tokens-Out / X-Forge-Duration-Ms / X-Forge-Model / X-Forge-Provider headers.
One accumulator is created per A2A invocation by the runner and stashed in context.Context. Every AfterLLMCall hook calls AddLLMCall to fold the current call's counts into the running totals. At response time the runner reads Snapshot() and stamps the headers.
Headers are the orchestration channel for real-time cost enforcement during parallel workflow execution. They populate regardless of whether OTel tracing is enabled — they're the orchestration channel, not the observability channel. See issue #87 / FWS-3.
func LLMUsageAccumulatorFromContext ¶
func LLMUsageAccumulatorFromContext(ctx context.Context) *LLMUsageAccumulator
LLMUsageAccumulatorFromContext returns the per-invocation accumulator from ctx, or nil when no accumulator was attached (e.g. internal cron-fire paths that don't need response headers).
func NewLLMUsageAccumulator ¶
func NewLLMUsageAccumulator() *LLMUsageAccumulator
NewLLMUsageAccumulator returns a fresh accumulator with its invocation clock started at the time of the call.
func (*LLMUsageAccumulator) AddLLMCall ¶
func (a *LLMUsageAccumulator) AddLLMCall(model, provider string, usage LLMUsage, duration time.Duration)
AddLLMCall folds one LLM call's usage + duration into the running totals. The most-recently-added call's model + provider become the "primary" reported in the X-Forge-Model / X-Forge-Provider headers, matching the issue's spec: "the primary model used (most recent if multiple)".
func (*LLMUsageAccumulator) Snapshot ¶
func (a *LLMUsageAccumulator) Snapshot() LLMUsageSnapshot
Snapshot returns the current totals. Safe to call from a goroutine different from AddLLMCall callers.
type LLMUsageSnapshot ¶
type LLMUsageSnapshot struct {
InputTokens int
OutputTokens int
LLMTimeTotal time.Duration // sum of per-LLM-call durations
InvocationDuration time.Duration // wall-clock since accumulator creation
PrimaryModel string
PrimaryProvider string
LLMCallCount int
}
LLMUsageSnapshot is an immutable readout of the accumulator's totals at a single point in time. Returned by Snapshot for use by the A2A response handler.
type LoadedKey ¶ added in v0.17.0
type LoadedKey struct {
Private ed25519.PrivateKey
Public ed25519.PublicKey
Kid string
}
LoadedKey pairs a private key with the operator-supplied key id. Exported so callers (runner, tests) can pass it around without re-parsing.
func LoadEd25519KeyFromEnv ¶ added in v0.17.0
LoadEd25519KeyFromEnv reads an Ed25519 private key from the given env var (base64-standard-encoded PKCS#8 DER OR PEM). Kid is loaded from the paired FORGE_AUDIT_SIGNING_KID env var (falls back to "forge-audit-v1" so a single-key deployment doesn't need to set two variables).
Returns (nil, nil) when the env var is unset — signing stays off, no error. This is intentional: adding the config to a deployment enables signing; absence keeps the pre-#213 behavior.
func LoadEd25519KeyFromFile ¶ added in v0.17.0
LoadEd25519KeyFromFile reads a PKCS#8 PEM Ed25519 key file. Path is expanded (~) before opening.
type Logger ¶
type Logger interface {
Info(msg string, fields map[string]any)
Warn(msg string, fields map[string]any)
Error(msg string, fields map[string]any)
Debug(msg string, fields map[string]any)
}
Logger defines the structured logging interface for the runtime.
type Memory ¶
type Memory struct {
// contains filtered or unexported fields
}
Memory manages per-task conversation history with token budget tracking.
func NewMemory ¶
NewMemory creates a Memory with the given system prompt and character budget. If maxChars is 0, the budget is computed from the model name using ContextBudgetForModel. If both maxChars and model are zero/empty, a default of 512K chars (~128K tokens) is used. The budget must comfortably exceed the per-message truncation cap so that a single tool result plus its surrounding messages fit without triggering aggressive trimming.
func (*Memory) Append ¶
func (m *Memory) Append(msg llm.ChatMessage)
Append adds a message to the conversation history and trims if over budget. Individual messages exceeding maxMessageChars are truncated as a safety net.
func (*Memory) LoadFromStore ¶
func (m *Memory) LoadFromStore(data *SessionData)
LoadFromStore restores memory state from a persisted SessionData. It runs the loaded messages through sanitizeMessages, which strips the two known kinds of corruption that cause strict providers to reject the recovered conversation:
- Orphaned tool_calls — assistant messages whose tool_calls have no matching tool result (Responses API: "No tool output found for function call").
- Empty assistant turns — assistant messages with both empty content AND no tool_calls (issue #131). The OpenAI chat-completions schema considers that shape invalid; Moonshot, hosted OpenRouter, and OpenAI strict mode return HTTP 400 if a recovered conversation contains one. Such turns appear when the provider hits `finish_reason: length` and the in-loop empty- response recovery fires — pre-#131 builds persisted the empty turn alongside the recovered real response. Stripping on load rescues sessions written by those builds without a migration.
func (*Memory) Messages ¶
func (m *Memory) Messages() []llm.ChatMessage
Messages returns the full message list with the system prompt prepended. If an existing summary is present (from compaction), it is appended to the system prompt so the LLM has prior context.
type MemoryFlusher ¶
MemoryFlusher is the interface for flushing observations to long-term memory. Implemented by memory.Manager.
type MemoryStore ¶
type MemoryStore struct {
// contains filtered or unexported fields
}
MemoryStore provides file-backed session persistence. Each session is stored as a JSON file in the configured directory.
func NewMemoryStore ¶
func NewMemoryStore(dir string) (*MemoryStore, error)
NewMemoryStore creates a MemoryStore backed by the given directory. The directory is created if it does not exist.
func (*MemoryStore) Cleanup ¶
func (s *MemoryStore) Cleanup(maxAge time.Duration) (int, error)
Cleanup removes sessions older than maxAge based on their UpdatedAt timestamp. Returns the number of sessions deleted.
func (*MemoryStore) Delete ¶
func (s *MemoryStore) Delete(taskID string) error
Delete removes a session file from disk.
func (*MemoryStore) List ¶
func (s *MemoryStore) List() ([]string, error)
List returns all session task IDs stored on disk.
func (*MemoryStore) Load ¶
func (s *MemoryStore) Load(taskID string) (*SessionData, error)
Load reads a SessionData from disk. Returns (nil, nil) if the session file does not exist.
func (*MemoryStore) Save ¶
func (s *MemoryStore) Save(data *SessionData) error
Save persists a SessionData to disk using atomic write (temp+fsync+rename). On the first write for a task, CreatedAt is set to now. On subsequent writes, the original CreatedAt is preserved from the existing file.
type ModelConfig ¶
type ModelConfig struct {
Provider string
Client llm.ClientConfig
Fallbacks []FallbackModelConfig
}
ModelConfig holds the resolved model provider and configuration.
func ResolveModelConfig ¶
func ResolveModelConfig(cfg *types.ForgeConfig, envVars map[string]string, providerOverride string) *ModelConfig
ResolveModelConfig resolves the LLM provider and configuration from multiple sources with the following priority (highest wins):
- CLI --provider flag (providerOverride)
- Environment variables: FORGE_MODEL_PROVIDER, OPENAI_API_KEY, ANTHROPIC_API_KEY, LLM_API_KEY
- forge.yaml model section
Returns nil if no provider could be resolved.
type NoopAdmissionChecker ¶ added in v0.16.0
type NoopAdmissionChecker struct{}
NoopAdmissionChecker is installed when the env-var pair (FORGE_ADMISSION_URL + FORGE_PLATFORM_TOKEN) is missing or incomplete. Every Admit returns an unconditional allow — the pre-#201 behavior. No platform call, no cache, no observability noise.
type NoopGuardrailChecker ¶
type NoopGuardrailChecker struct{}
NoopGuardrailChecker is a passthrough implementation that performs no checks. Used as a fallback when no guardrail configuration is available.
func (*NoopGuardrailChecker) CheckContext ¶
func (*NoopGuardrailChecker) CheckInbound ¶
func (n *NoopGuardrailChecker) CheckInbound(_ context.Context, _ *a2a.Message) (PolicyResult, error)
func (*NoopGuardrailChecker) CheckOutbound ¶
func (n *NoopGuardrailChecker) CheckOutbound(_ context.Context, _ *a2a.Message) (PolicyResult, error)
func (*NoopGuardrailChecker) CheckStream ¶
func (*NoopGuardrailChecker) CheckToolCall ¶
func (*NoopGuardrailChecker) CheckToolOutput ¶
type PlatformCommandGuard ¶ added in v0.17.1
type PlatformCommandGuard struct {
// contains filtered or unexported fields
}
PlatformCommandGuard enforces operator-authored command-deny patterns on EVERY tool call, regardless of the active skill (#238 / ASI02). It is the platform-layer sibling of SkillGuardrailEngine's deny_commands and reuses the same match-target semantics (canonicalizeToolInput): cli_execute → the reconstructed command line, any other tool → the raw tool-input JSON.
Unlike NewSkillGuardrailEngine, which SKIPS an invalid regex with a warning, this guard fails closed: NewPlatformCommandGuard returns an error on the first uncompilable pattern so the runner can abort startup — an operator policy must never silently drop a rule.
func NewPlatformCommandGuard ¶ added in v0.17.1
func NewPlatformCommandGuard(specs []PlatformCommandSpec) (*PlatformCommandGuard, error)
NewPlatformCommandGuard compiles the resolved (unioned) platform command specs. It returns an error naming the offending pattern + layer on the first regex that fails to compile, so startup aborts loudly rather than enforcing a partial policy. A nil/empty spec slice yields an empty guard.
func (*PlatformCommandGuard) Empty ¶ added in v0.17.1
func (g *PlatformCommandGuard) Empty() bool
Empty reports whether the guard has no patterns (nil-safe) — lets the runner skip registering the BeforeToolExec hook entirely.
func (*PlatformCommandGuard) Match ¶ added in v0.17.1
func (g *PlatformCommandGuard) Match(toolName, toolInput string) *PlatformCommandMatch
Match returns the first pattern (in layer order) whose regex matches the canonicalized tool input, or nil when the call is allowed. The match target is identical to skill deny_commands so operators and skill authors author patterns the same way.
type PlatformCommandMatch ¶ added in v0.17.1
type PlatformCommandMatch struct {
Pattern string
Message string
LayerSource string
LayerPath string
}
PlatformCommandMatch is returned by PlatformCommandGuard.Match when a tool call's arguments match an operator pattern. It names the offending pattern, the operator's custom message (if any), and the first-denying layer.
type PlatformCommandSpec ¶ added in v0.17.1
type PlatformCommandSpec struct {
Pattern string
Message string
LayerSource string // "system" / "user" / "workspace"
LayerPath string
}
PlatformCommandSpec is one operator-authored command-deny pattern from the platform policy layers (#238), before compilation. The Layer* fields carry attribution so a runtime block can name the policy that forbade the command in the audit stream.
type PolicyDecision ¶ added in v0.17.0
type PolicyDecision int
PolicyDecision is the taxonomy of decisions a Forge policy engine (guardrails, admission, skill rules) can produce for one evaluated piece of content. Governance R4 requires the engine to be capable of expressing each of these — even if a particular gate only exercises a subset today. See docs/security/policy-decisions.md.
Constants are ordered by RESTRICTIVENESS — the ordinal value maps to severity so `partA.Decision > partB.Decision` selects the more restrictive decision when aggregating across multiple parts:
Allow < Modify < StepUp < Defer < Deny
Callers comparing severity SHOULD use the ordinal directly. Do NOT reorder without updating every aggregate site (see LibraryGuardrailEngine.CheckOutbound and its per-part escalation).
const ( // DecisionAllow — content passes through unmodified. Zero value. // Least restrictive. DecisionAllow PolicyDecision = iota // DecisionModify — content is admissible but must be rewritten // (redacted, truncated, tagged) before it moves forward. // PolicyResult.Modified carries the replacement. DecisionModify // DecisionStepUp — content is conditionally admissible pending an // additional user/operator interaction (re-auth, approval, // verification). Reserved for R4b (#210). DecisionStepUp // DecisionDefer — decision requires an out-of-band lookup // (platform API, human queue) before the caller can proceed. // Reserved for R4c (#211). DecisionDefer // DecisionDeny — content is rejected. Caller MUST propagate the // error and MUST NOT let the content proceed. Most restrictive. DecisionDeny )
func (PolicyDecision) String ¶ added in v0.17.0
func (d PolicyDecision) String() string
String returns the audit-safe decision token. Matches the strings emitted on guardrail_check events so a SIEM index built from those events can be queried by PolicyDecision value.
type PolicyResult ¶ added in v0.17.0
type PolicyResult struct {
Decision PolicyDecision
Modified string
Reason string
// RequiredAcr is populated only when Decision == DecisionStepUp
// (governance R4b / #210). Names the auth-context class the
// caller MUST re-authenticate under before the action is admitted.
// The runner turns this into an RFC 9470 WWW-Authenticate challenge
// on the 401 response: `Bearer error="step_up_required",
// acr_values="<value>"`. Consumers (SDKs, browsers) trigger a
// higher-assurance authentication and retry.
//
// Typical values follow the ACR conventions of the caller's IdP:
// - "acr:mfa" — arbitrary MFA method acceptable
// - "urn:mace:incommon:iap:silver" — InCommon Silver
// - "0"/"1"/"2" — SAML/oidc-style tier numbers
// Forge doesn't interpret the value; it just relays it end-to-end
// so operator + IdP + caller agree on the semantics.
RequiredAcr string
// Defer carries the R4c (#211) deferral parameters. Populated
// only when Decision == DecisionDefer. See runtime.DeferSpec.
Defer *DeferSpec
}
PolicyResult carries the outcome of one policy evaluation.
For DecisionAllow / DecisionDeny, Modified is empty. For DecisionModify, Modified holds the rewritten content; callers substitute it into the value stream. For DecisionStepUp, RequiredAcr names the auth-context class the caller must re-authenticate under (see #210 / R4b). For DecisionDefer, Defer describes the out-of-band decision — target, timeout, context (see #211 / R4c). Reason is a short human-readable string surfaced on audit events and (for Deny / StepUp) returned to the caller as an error message.
func Defer ¶ added in v0.17.0
func Defer(to string, timeout time.Duration, contextForApprover, reason string) PolicyResult
Defer constructs an R4c deferred result. `to`, `timeout` and `contextForApprover` name the target, wait ceiling, and payload the human approver sees.
func Deny ¶ added in v0.17.0
func Deny(reason string) PolicyResult
Deny constructs a rejection result carrying `reason`.
func Modify ¶ added in v0.17.0
func Modify(newContent, reason string) PolicyResult
Modify constructs a redact-and-continue result.
func StepUp ¶ added in v0.17.0
func StepUp(requiredAcr, reason string) PolicyResult
StepUp constructs a step-up-required result. The `acr` is relayed to the caller in the RFC 9470 challenge header. See #210 / R4b.
type ProgressEmitter ¶
type ProgressEmitter func(event ProgressEvent)
ProgressEmitter is a callback that emits progress events to the client.
func ProgressEmitterFromContext ¶
func ProgressEmitterFromContext(ctx context.Context) ProgressEmitter
ProgressEmitterFromContext retrieves the ProgressEmitter from the context, or nil.
type ProgressEvent ¶
ProgressEvent describes a progress update during task execution.
type RemoteSessionStore ¶ added in v0.17.0
type RemoteSessionStore struct {
// contains filtered or unexported fields
}
RemoteSessionStore is the opt-in SessionStore backend (issue #243). It pushes per-task snapshots to a platform session service over HTTP so agent pods stay stateless — any replica can resume any task with no shared filesystem / PVC.
Wire contract (the platform session service — agent-builder — is the server side):
GET {base}/{taskID}?agent_id=<id> If-None-Match: "<ver>"
-> 200 + ETag: "<ver>" + SessionData (fresh)
-> 304 (caller's cached copy is current)
-> 404 (no session yet)
PUT {base}/{taskID}?agent_id=<id> If-Match: "<ver>" + SessionData
-> 200 + ETag: "<newVer>" (committed)
-> 412 (version conflict / CAS fail)
DELETE {base}/{taskID}?agent_id=<id> -> 2xx
Concurrency model (per issue #243): A2A serializes turns within a task, so there is normally one writer per task at a time. The store keeps the ETag it last saw per task and uses it as the conditional header — a conditional GET avoids re-pulling unchanged state (304), and an If-Match PUT turns the rare rolling-deploy overlap window into a detectable 412 instead of a silent lost update. On 412 the store YIELDS — it surfaces ErrConflict rather than re-PUTting its (now stale) snapshot, so the concurrent writer's committed turn is never clobbered. The loop treats that as a best-effort-persist failure: it logs and moves on, the newer state wins, and the model is never re-run. The store's cache self-heals on the next turn's conditional GET.
Auth mirrors the admission client exactly: Bearer FORGE_PLATFORM_TOKEN plus the Org-Id / Workspace-Id tenancy headers (omitted when empty).
func NewRemoteSessionStore ¶ added in v0.17.0
func NewRemoteSessionStore(cfg RemoteSessionStoreConfig) *RemoteSessionStore
NewRemoteSessionStore builds a remote-backed SessionStore. It performs no network call at construction time; the first Load/Save hits the platform.
func (*RemoteSessionStore) Delete ¶ added in v0.17.0
func (r *RemoteSessionStore) Delete(taskID string) error
Delete removes the session for taskID and drops it from the cache.
func (*RemoteSessionStore) Load ¶ added in v0.17.0
func (r *RemoteSessionStore) Load(taskID string) (*SessionData, error)
Load fetches the session for taskID with a conditional GET. A cached ETag (from a prior Load/Save on this pod) drives If-None-Match: a 304 returns the cached snapshot without re-downloading; a 200 refreshes the cache; a 404 means no session yet (nil, nil). On any transport / server error it returns the error and the loop falls back to task.History, exactly as a cold session would.
func (*RemoteSessionStore) Save ¶ added in v0.17.0
func (r *RemoteSessionStore) Save(data *SessionData) error
Save commits a full snapshot. It sends If-Match with the last-seen version so a concurrent writer's intervening commit is caught as a 412. On 412 it YIELDS (returns ErrConflict) rather than re-PUTting: this store holds only a full snapshot built from the stale version, so a blind retry would overwrite the other writer's committed turn — the exact lost update the If-Match exists to detect. A message-level rebase isn't available (and is semantically fragile for a conversation), so the correct resolution is to let the newer state win. The model is never re-run; the cached ETag self-heals on the next conditional GET.
type RemoteSessionStoreConfig ¶ added in v0.17.0
type RemoteSessionStoreConfig struct {
BaseURL string
AgentID string
OrgID string
WorkspaceID string
PlatformToken string
Logger Logger
// HTTPClient overrides the default client (tests inject one).
HTTPClient *http.Client
}
RemoteSessionStoreConfig configures a RemoteSessionStore. AgentID is required (the platform routes on it). OrgID / WorkspaceID are optional (empty -> header omitted). PlatformToken is the reusable Forge -> platform bearer (FORGE_PLATFORM_TOKEN), same token the admission client sends.
type SequenceCounter ¶
SequenceCounter is the per-invocation atomic counter that drives AuditEvent.Sequence. One counter per A2A invocation; stuffed into the request context by the A2A handler at request entry, read by EmitFromContext (and any emit-from-context helper) to stamp the next sequence number.
Type alias for *atomic.Int64 so callers can construct one with `new(atomic.Int64)` and so the package stays small.
func SequenceCounterFromContext ¶
func SequenceCounterFromContext(ctx context.Context) *SequenceCounter
SequenceCounterFromContext returns the per-invocation counter, or nil if none was set. The audit emit path uses nil-vs-non-nil to decide whether to stamp a Sequence on outbound events.
type SequenceRegistry ¶ added in v0.18.1
type SequenceRegistry struct {
// contains filtered or unexported fields
}
SequenceRegistry maps a live per-invocation SequenceCounter to its (correlation_id, task_id) key so events emitted OUTSIDE the request goroutine's context can advance the SAME counter and stamp a correct, gap-free seq. Two emitters need this:
- The egress proxy (#341): a separate 127.0.0.1 forward proxy with no request ctx. It recovers (correlation_id, task_id) from the subprocess Proxy-Authorization creds (#338) and looks the counter up here.
- The MCP consent-resume paths (#366): the loopback OAuth callback and the platform POST /mcp/consent run on a detached browser/platform request; they recover the parked call's (correlation_id, task_id) and seed a ctx from the registered counter so the completion egress is seq'd + attributed.
The counter is registered at request entry (alongside EnsureSequenceCounter) and evicted at invocation_complete. A miss returns 0 (the event stays seq-less rather than carrying a wrong or duplicated number).
func NewSequenceRegistry ¶ added in v0.18.1
func NewSequenceRegistry() *SequenceRegistry
NewSequenceRegistry returns an empty registry.
func (*SequenceRegistry) Evict ¶ added in v0.18.1
func (r *SequenceRegistry) Evict(correlationID, taskID string)
Evict drops the registration for (correlationID, taskID).
func (*SequenceRegistry) Get ¶ added in v0.18.1
func (r *SequenceRegistry) Get(correlationID, taskID string) *SequenceCounter
Get returns the counter for (correlationID, taskID), or nil if none is registered.
func (*SequenceRegistry) NextSequenceFor ¶ added in v0.18.1
func (r *SequenceRegistry) NextSequenceFor(correlationID, taskID string) int64
NextSequenceFor advances the registered counter for (correlationID, taskID) and returns the new seq, or 0 when no counter is registered (so the caller JSON-omits the field rather than emitting a wrong/duplicate seq).
func (*SequenceRegistry) Register ¶ added in v0.18.1
func (r *SequenceRegistry) Register(correlationID, taskID string, c *SequenceCounter)
Register records the counter under (correlationID, taskID). No-op on a nil registry/counter or an all-empty key.
type SessionData ¶
type SessionData struct {
TaskID string `json:"task_id"`
Messages []llm.ChatMessage `json:"messages"`
Summary string `json:"summary,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
SessionData holds the persisted state for a single task's conversation.
type SessionStore ¶ added in v0.17.0
type SessionStore interface {
// Load returns the persisted session for taskID, or (nil, nil) when
// none exists.
Load(taskID string) (*SessionData, error)
// Save persists a full snapshot for data.TaskID.
Save(data *SessionData) error
// Delete removes the persisted session for taskID (no error if absent).
Delete(taskID string) error
}
SessionStore persists per-task conversation state (SessionData), keyed by task ID. Two backends implement it:
- file (default): *MemoryStore — today's local .forge/sessions/*.json. Single-pod / dev. Versionless; every Load returns current state.
- remote (opt-in): *RemoteSessionStore — pushes snapshots to a platform session service so stateless pods can resume any task on any replica without a PVC (issue #243).
The method set deliberately mirrors *MemoryStore's existing methods so the executor and compactor depend only on this interface and the file backend satisfies it with no adapter. Version / conditional-GET / CAS bookkeeping is an implementation concern of the remote backend (it tracks the per-task ETag it last saw), NOT of the caller — the loop keeps calling Load/Save/Delete exactly as before.
type Sink ¶
type Sink interface {
// Write delivers a single event. The event is already marshaled
// NDJSON (one line, trailing newline included). Returns nil even
// on transient failure; sinks are responsible for their own
// retry/buffering policy. A non-nil error indicates a permanent
// sink failure that should be logged once.
Write(ctx context.Context, eventBytes []byte) error
// Close flushes any buffered events and releases resources. Called
// during agent shutdown. Implementations must honor any deadline
// on the passed context and never block beyond it.
Close(ctx context.Context) error
// Name returns a stable identifier ("stderr" / "unix-socket" /
// "localhost-http") used in self-reporting and operator logs.
Name() string
// Stats returns counters describing sink health since process
// start. Keys are stable strings (writes_ok, drops_timeout,
// drops_dial, connected); values are monotonic counts or 0/1
// flags. Used by the periodic audit_export_status emitter and
// the /health endpoint.
Stats() map[string]int64
}
Sink consumes serialized audit event bytes. Implementations must be safe for concurrent use. Sinks should never block the emitter under back-pressure for longer than their configured timeout; on timeout the sink drops the event and increments its drop counter, never returns an error to the caller.
The audit pipeline composes one or more sinks (stderr safety-net + optional Unix socket / HTTP sink for export). Each sink is independent; a failure on one does not stop emission on the others.
See issue #95 / FWS-7.
func NewHTTPSink ¶
NewHTTPSink constructs a localhost HTTP sink. Returns nil if endpoint is empty. The http.Client is built with a per-request timeout matching the write timeout; the transport defaults are fine — no keep-alive tuning needed because we expect one POST per emit and localhost RTT is sub-millisecond.
func NewSocketSink ¶
NewSocketSink constructs a Unix Domain Socket sink. Zero values for writeTimeout / dialTimeout fall back to defaults. The socket is NOT dialed eagerly — the first Write triggers the connection attempt.
Returns nil if path is empty (caller should not register an empty sink). Path validation (length, parent dir exists) is deliberately deferred to dial time: a sidecar that creates its socket lazily shouldn't cause the agent to fail at startup.
type SkillGuardrailEngine ¶
type SkillGuardrailEngine struct {
// contains filtered or unexported fields
}
SkillGuardrailEngine enforces skill-declared deny patterns on command inputs, tool outputs, and user prompts. It complements the global GuardrailEngine with domain-specific rules authored by skill developers.
func NewSkillGuardrailEngine ¶
func NewSkillGuardrailEngine(rules *agentspec.SkillGuardrailRules, enforce bool, logger Logger) *SkillGuardrailEngine
NewSkillGuardrailEngine creates a SkillGuardrailEngine from aggregated skill rules. Invalid regex patterns are skipped with a warning.
func (*SkillGuardrailEngine) CheckCommandInput ¶
func (s *SkillGuardrailEngine) CheckCommandInput(toolName, toolInput string) error
CheckCommandInput validates a tool call before execution.
For cli_execute, the JSON is parsed to build a "binary arg1 arg2..." canonical string so operators can author shell-style patterns (`git.*--force`). For any other tool, the raw tool-input JSON is used as the match target — so `deny_commands` can match on payload fragments of http_request / mcp / builtin tool calls too. This generalizes MODIFY/DENY beyond the cli_execute-only path called out in governance R4a (#209).
func (*SkillGuardrailEngine) CheckCommandOutput ¶
func (s *SkillGuardrailEngine) CheckCommandOutput(toolName, toolOutput string) (string, error)
CheckCommandOutput validates tool output after execution.
Applies deny_output patterns to the output of ANY tool — not just cli_execute. Governance R4a (#209) requires MODIFY (redact) to be available for all tool outputs, not a special-cased path. The pattern-matching loop is delegated to applyOutputPolicy so other call sites (LLM response scanning, future MCP tool result hook) can reuse the same block/redact semantics.
func (*SkillGuardrailEngine) CheckLLMResponse ¶
func (s *SkillGuardrailEngine) CheckLLMResponse(text string) (string, bool)
CheckLLMResponse validates the LLM's response text against deny_responses patterns. When a match is found, the response is replaced with the skill-defined redirect message to prevent binary/tool enumeration leaks. Returns the (possibly replaced) text and whether a replacement occurred.
func (*SkillGuardrailEngine) CheckUserInput ¶
func (s *SkillGuardrailEngine) CheckUserInput(text string) error
CheckUserInput validates a user message against deny_prompts patterns. Returns an error with the skill-defined redirect message if the prompt matches.
type TenancyContext ¶
TenancyContext carries the org / workspace identifiers a Forge agent extracts from inbound A2A request headers. Zero value is meaningful — it means "no per-request override; fall back to whatever the AuditLogger's static stamp says."
func TenancyContextFromContext ¶
func TenancyContextFromContext(ctx context.Context) TenancyContext
TenancyContextFromContext retrieves the TenancyContext from the context. Returns the zero value (IsZero == true) when none was set, which is the signal EmitFromContext uses to fall back to the AuditLogger's static tenancy stamp.
func TenancyContextFromHTTPHeaders ¶
func TenancyContextFromHTTPHeaders(h http.Header) TenancyContext
TenancyContextFromHTTPHeaders extracts X-Forge-Org-ID and X-Forge-Workspace-ID from an inbound HTTP request's headers. Missing headers map to empty fields; the returned TenancyContext is IsZero when neither is set. Mirrors WorkflowContextFromHTTPHeaders — same pattern, same precedence rules at the call site.
func (TenancyContext) ApplyToHTTPHeaders ¶
func (t TenancyContext) ApplyToHTTPHeaders(h http.Header)
ApplyToHTTPHeaders writes any non-empty TenancyContext fields onto outbound request headers. Used by tools that explicitly propagate tenancy to downstream A2A calls in an agent-to-agent flow. Auto-propagation is NOT built into the egress proxy — same rationale as WorkflowContext: a tenancy header would leak if the agent called a non-Forge third party. Tools propagate explicitly when they know the target is a tenancy-aware peer.
func (TenancyContext) IsZero ¶
func (t TenancyContext) IsZero() bool
IsZero reports whether the TenancyContext carries no overrides. EmitFromContext checks this before reaching for the AuditLogger's static stamp.
type ToolExecutor ¶
type ToolExecutor interface {
Execute(ctx context.Context, name string, arguments json.RawMessage) (string, error)
ToolDefinitions() []llm.ToolDefinition
}
ToolExecutor provides tool execution capabilities to the engine. The tools.Registry satisfies this interface via Go structural typing.
type VerifyOptions ¶ added in v0.17.0
type VerifyOptions struct {
// Pubkeys is a map from Kid to Ed25519 public key. When empty,
// signature verification is skipped entirely. Events that carry
// a Sig field are still walked structurally + chain-verified, and
// a soft-warning note is added to Errors so operators know the
// signatures were not checked.
Pubkeys map[string]ed25519.PublicKey
// SkipChain, when true, verifies signatures only and does NOT
// walk the hash chain. Useful for tooling that ingests a stream
// mid-flight (SIEM tail) where the head-of-stream genesis is
// out of view. Default false — full tamper-evidence checks
// require both chain and signature verification.
SkipChain bool
}
VerifyOptions configures VerifyAuditLog.
type VerifyResult ¶ added in v0.17.0
type VerifyResult struct {
// EventCount is the total number of well-formed events read.
EventCount int
// FirstBadLine is the 1-indexed input line at which verification
// first failed. Zero when the whole stream verifies.
FirstBadLine int
// BadEvent is the parsed body of the bad event (best effort —
// may be empty if the line failed to parse).
BadEvent AuditEvent
// Reason is a short, human-readable classification of the failure.
// Empty when OK.
Reason string
// SigChecked counts how many events had their Ed25519 signature
// verified (only when the caller supplied a pubkey source).
SigChecked int
// ChainChecked counts how many events had their prev_hash link
// verified against the previous line's computed hash.
ChainChecked int
// GenesisSeen is true when the first well-formed event carried
// PrevHash == AuditChainGenesis. A false value on a non-empty
// stream signals HEAD truncation (the run's initial event has
// been stripped) — surfaced via Errors as a soft warning, not
// a hard failure, because a partial-stream fragment is a
// legitimate use case for external SIEMs that ingest continuously.
GenesisSeen bool
// Errors accumulates non-fatal issues that don't stop verification.
// Populated even on OK() streams — used for head-truncation
// warnings and "signed events but no --pubkey" notes.
Errors []string
}
VerifyResult summarizes the outcome of walking an NDJSON audit stream. See VerifyAuditLog.
func VerifyAuditLog ¶ added in v0.17.0
func VerifyAuditLog(r io.Reader, opts VerifyOptions) (VerifyResult, error)
VerifyAuditLog walks an NDJSON audit stream and reports the first integrity failure it can detect:
- Malformed JSON on any line.
- `prev_hash` mismatch (chain break) — the current event's prev_hash doesn't equal sha256 of the previous line's raw bytes (excluding the trailing newline). This catches tampering (altered fields, added bytes) and deletion (dropped events).
- Ed25519 signature mismatch (when Pubkeys is non-empty and the event carries a Sig field).
Hashing is over the RAW line bytes as read from the stream, not over a re-marshaled event — the producer already committed to specific bytes and the verifier should not reconstruct them. This closes the "large-integer precision" hole where json.Marshal(json.Unmarshal(x)) is not a fixed point when Fields carries values > 2^53 (they decode to float64 and re-marshal differently).
Never panics on malformed input. Reads to EOF or until the first failure; malformed lines are treated as hard failures because audit consumers must see them (they signal either producer bugs or intentional tampering).
func (VerifyResult) OK ¶ added in v0.17.0
func (r VerifyResult) OK() bool
OK reports whether the stream verified end-to-end.
type WorkflowContext ¶
type WorkflowContext struct {
// WorkflowID identifies the workflow DEFINITION — stable across
// all runs of the same workflow. Sourced from X-Workflow-ID.
// Audit consumers join on this for definition-level rollups:
// "top failing workflows," "latency by workflow definition."
WorkflowID string
// WorkflowExecutionID identifies the PER-RUN instance — unique
// per workflow execution. Sourced from X-Workflow-Execution-ID.
// Audit consumers join on this for per-run timelines: "show me
// every event in this specific run, across every agent the
// orchestrator dispatched to." Added in FORGE-2 / issue #185.
WorkflowExecutionID string
// StageID identifies a stage within the workflow (a group of
// steps that may run in parallel).
StageID string
// StepID identifies the specific step within the stage that
// invoked this agent.
StepID string
// InvocationCaller identifies the upstream caller — typically the
// orchestrator's identity, but for agent-to-agent calls within a
// workflow it carries the upstream agent's identifier.
InvocationCaller string
}
WorkflowContext carries the orchestration identifiers a Forge agent extracts from inbound A2A request headers. Zero value is meaningful — it represents "no workflow context" (direct A2A invocation).
func WorkflowContextFromContext ¶
func WorkflowContextFromContext(ctx context.Context) WorkflowContext
WorkflowContextFromContext retrieves the WorkflowContext from the context. Returns the zero value (IsZero == true) when none was set.
func WorkflowContextFromHTTPHeaders ¶
func WorkflowContextFromHTTPHeaders(h http.Header) WorkflowContext
WorkflowContextFromHTTPHeaders extracts the orchestration identifiers from an inbound HTTP request's headers. Missing headers map to empty fields; the returned WorkflowContext is `IsZero` when none are set.
func (WorkflowContext) ApplyToHTTPHeaders ¶
func (w WorkflowContext) ApplyToHTTPHeaders(h http.Header)
ApplyToHTTPHeaders writes any non-empty WorkflowContext fields onto outbound request headers. Used by tools that explicitly propagate workflow context to downstream A2A calls (the issue's "agent invoking another agent during workflow execution" path).
Auto-propagation is deliberately not built into the egress proxy — the X-Workflow-* headers identify the workflow and would leak if the agent calls a non-workflow third-party API. Tools propagate explicitly when they know the target is a workflow peer.
func (WorkflowContext) IsZero ¶
func (w WorkflowContext) IsZero() bool
IsZero reports whether the WorkflowContext carries no orchestration identifiers. Used by audit and helpers to decide whether to stamp workflow fields (when zero, fields are omitted entirely so the emitted JSON matches the pre-FWS-2 shape).
type WorkflowPropagationMatcher ¶ added in v0.16.0
type WorkflowPropagationMatcher struct {
// contains filtered or unexported fields
}
WorkflowPropagationMatcher decides whether a given outbound host should auto-receive the workflow headers. Mirrors the wildcard semantics of security.DomainMatcher (exact + `*.suffix.com`) but is kept independent so this package doesn't grow a dependency on forge-core/security just for matching.
func NewWorkflowPropagationMatcher ¶ added in v0.16.0
func NewWorkflowPropagationMatcher(hosts []string) *WorkflowPropagationMatcher
NewWorkflowPropagationMatcher parses an allow-list spec into a matcher. Empty / nil input returns a matcher whose Matches() always returns false — explicit safe default. Each entry is normalized to lowercase and trimmed; blank entries are skipped.
Entries beginning with `*.` register as wildcard suffix patterns; every other entry is an exact host. There is deliberately no other pattern syntax — Forge's hostname surface is small enough that exact + suffix covers the meaningful cases and matches the existing egress allow-list shape.
func (*WorkflowPropagationMatcher) IsEmpty ¶ added in v0.16.0
func (m *WorkflowPropagationMatcher) IsEmpty() bool
IsEmpty reports whether the matcher would never match any host (no exact entries, no wildcards). Lets callers short-circuit the transport wrap entirely in the default-deploy path.
func (*WorkflowPropagationMatcher) Matches ¶ added in v0.16.0
func (m *WorkflowPropagationMatcher) Matches(host string) bool
Matches reports whether the given host is in the allow-list. The host argument may include a `:port` suffix (e.g. URL.Host on a custom-port request) — the matcher strips the port before comparing because propagation decisions are per-host, not per-port.
A nil receiver returns false so callers can use the matcher pattern without nil-guarding: `m.Matches(host)` is safe even on an uninitialized matcher.
Source Files
¶
- admission.go
- agentcard.go
- agentcard_security.go
- agentcard_skills.go
- audit.go
- audit_export_config.go
- audit_export_status.go
- audit_payload_capture.go
- audit_schema.go
- audit_signing.go
- audit_sink.go
- audit_sink_http.go
- audit_sink_socket.go
- audit_verify.go
- cancellation.go
- config.go
- content_redact.go
- env.go
- executor.go
- guardrails.go
- hooks.go
- logger.go
- loop.go
- memory.go
- memory_compactor.go
- memory_store.go
- platform_command_guard.go
- remote_session_store.go
- runtime.go
- session_store.go
- skill_guardrails.go
- summary.go
- tenancy.go
- tracing.go
- usage_accumulator.go
- workflow.go
- workflow_propagation.go