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 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 PrepareSpanContent(s string, redact bool, maxBytes int) string
- 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 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
- 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) Sinks() []Sink
- func (a *AuditLogger) WithEntity(entityType, entityID string) *AuditLogger
- func (a *AuditLogger) WithTenancy(orgID, workspaceID string) *AuditLogger
- type AuditPayloadCapture
- type CancellationReason
- type CancellationRegistry
- type Compactor
- type CompactorConfig
- type FallbackModelConfig
- type GuardrailChecker
- type Hook
- type HookContext
- type HookPoint
- type HookRegistry
- type JSONLogger
- type LLMCallAuditArgs
- type LLMExecutor
- type LLMExecutorConfig
- type LLMUsage
- type LLMUsageAccumulator
- type LLMUsageSnapshot
- type Logger
- type Memory
- type MemoryFlusher
- type MemoryStore
- type ModelConfig
- type NoopGuardrailChecker
- func (n *NoopGuardrailChecker) CheckContext(_ context.Context, content string) (string, error)
- func (n *NoopGuardrailChecker) CheckInbound(_ context.Context, _ *a2a.Message) error
- func (n *NoopGuardrailChecker) CheckOutbound(_ context.Context, _ *a2a.Message) 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 ProgressEmitter
- type ProgressEvent
- type SequenceCounter
- type SessionData
- 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 WorkflowContext
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) // 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" // 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" // 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" // 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 ( 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" 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 / stage / step identifiers so audit consumers can correlate events across multiple agents participating in the same workflow.
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 AuditExportStatus = "audit_export_status"
AuditExportStatus is the event type for the periodic per-sink health report. Single event per tick; carries one entry in fields.sinks per registered sink with that sink's counters (writes_ok, drops_timeout, drops_dial, connected).
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 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 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.
Variables ¶
var AuditExportStatusInterval = 60 * time.Second
AuditExportStatusInterval is how often StartAuditExportStatus emits an audit_export_status event. The issue calls for 60s; exposed as a package var so tests can shorten it.
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 ExtractText ¶
ExtractText extracts all text parts from a message into a single string.
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 PrepareSpanContent ¶
PrepareSpanContent runs the redact (when redact=true) and byte-cap-with-truncation-marker pipeline for content destined for an OTel span attribute. The pipeline is:
- Apply RedactSecrets when redact=true.
- TruncateForAudit (the same byte-cap helper the audit path uses) so a runaway prompt can't blow past the backend attribute limit and silently drop the marker.
maxBytes <= 0 falls back to DefaultSpanContentCapBytes. The truncation marker is identical to what AuditPayloadCapture writes, so an operator who sees a `…[truncated:N]` suffix on an audit payload-captured field sees the same suffix on the linked span attribute for the same logical event.
Returns the empty string when s is empty (skipping the pipeline).
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 one audit_export_status event every AuditExportStatusInterval until the returned stop function is called or ctx is cancelled. The goroutine uses the same AuditLogger it reports on — the status event itself flows through every sink, including the export ones, so operators can see "is my export healthy?" by inspecting the export stream.
Returns a stop func the caller invokes during shutdown. The stop func blocks until the goroutine exits so shutdown ordering is deterministic (no chance of a final status event landing after the runtime has torn down its writers).
Idempotent: calling stop twice is safe.
See issue #95 / FWS-7 acceptance criterion §6.
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 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.
Types ¶
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"`
// 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 orchestrator-level workflow run that
// invoked this agent. Sourced from X-Workflow-ID at request entry;
// absent for direct A2A invocations.
WorkflowID string `json:"workflow_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 and the library's
// MongoDB GuardrailAuditEvent collection share columns 1:1 and
// can be joined 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, 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.
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) 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
// 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 field 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 must ensure the audit transport (the FWS-7 sink or the stderr safety net) lands in a store that respects the captured payloads' sensitivity (PII, secrets that may end up in prompts, etc.). The Forge codebase does not redact; what flows from the LLM call site flows verbatim into the event.
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 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 *MemoryStore
// 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 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.
CheckInbound(ctx context.Context, msg *a2a.Message) error
// CheckOutbound validates an outbound (agent) message —
// OutputGate. Implementations should prefer redacting sensitive
// content over blocking.
CheckOutbound(ctx context.Context, msg *a2a.Message) 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 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
// 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 *MemoryStore
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)
// 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 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 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 (*NoopGuardrailChecker) CheckOutbound ¶
func (*NoopGuardrailChecker) CheckStream ¶
func (*NoopGuardrailChecker) CheckToolCall ¶
func (*NoopGuardrailChecker) CheckToolOutput ¶
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 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 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 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. It only fires for cli_execute tool calls. Returns an error if the command matches a deny pattern.
func (*SkillGuardrailEngine) CheckCommandOutput ¶
func (s *SkillGuardrailEngine) CheckCommandOutput(toolName, toolOutput string) (string, error)
CheckCommandOutput validates tool output after execution. It only fires for cli_execute tool calls. Returns the (possibly redacted) output and an error if the output matches a "block" pattern.
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 WorkflowContext ¶
type WorkflowContext struct {
// WorkflowID identifies the orchestrator-level workflow run.
WorkflowID 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).
Source Files
¶
- 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_sink.go
- audit_sink_http.go
- audit_sink_socket.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
- runtime.go
- skill_guardrails.go
- summary.go
- tenancy.go
- tracing.go
- usage_accumulator.go
- workflow.go