v0.15.0
Forge v0.15.0 — Kubernetes-native scheduling, end-to-end guardrails audit, multi-tenant stamping
Forge v0.15.0 ships three substantial additions to the open-source AI agent runtime: a hybrid scheduler that deploys agent cron jobs as native Kubernetes CronJob resources, a complete guardrails audit pipeline covering all five library gates with optional OpenTelemetry span parity, and tenant + entity identifiers stamped on every audit event so SIEM consumers can filter the export stream by org, workspace, and agent without joining against authentication rows.
Forge agents now deploy to Kubernetes with one command: forge build && forge package && kubectl apply -k. The scheduler's CronJob controller takes over timing, the cluster's etcd persists state, and the manifests forge package emits are byte-identical to what the runtime backend reconciles — no drift between build-time and runtime.
This release also publishes forge-core, forge-skills, and forge-plugins as importable Go modules under their own path-prefixed tags so platform teams can embed Forge's runtime, registry, and channel adapters directly.
Highlights
scheduler.Backendinterface with two implementations: a file-backed scheduler (the existing 30s ticker + markdown persistence, unchanged behavior) and a Kubernetes backend usingk8s.io/client-gofor nativeCronJobCRUD. Selected at startup viascheduler.backend: auto | file | kubernetesinforge.yaml. In-cluster detection probes the projected ServiceAccount token at/var/run/secrets/kubernetes.io/serviceaccount/token.forge packageKubernetes manifest generation. For everyforge.yaml schedules[]entry, the build pipeline emits onecronjob-<id>.yaml, a credential-less Secret template, and a Role/RoleBinding scoped to the agent's namespace. The Secret manifest deliberately ships without adatafield — the credential never lands in version control; operators populate viaforge auth secret-yaml | kubectl apply -f -, ExternalSecrets, Sealed Secrets, SOPS, or Vault Agent Injector.forge authsubcommand (show-token,mint-token,secret-yaml) for the internal bearer token the runtime mints at startup. Same token channel adapters use for A2A callbacks, now reused by Kubernetes schedulerCronJobtrigger pods.guardrail_checkaudit event emitted at all five library gates (input,context,tool_call,output,stream) with opt-in evidence capture controlled byFORGE_GUARDRAIL_CAPTURE_EVIDENCE. The PII/secret content the library produces is captured post-mask by default; redact-then-truncate uses the same regex set as the OpenTelemetry content-capture pipeline.- OpenTelemetry guardrail spans (
guardrail.<gate>) shipped symmetrically to the audit emission. Block decisions stamp OTelErrorstatus with the violation summary — operators see blocked invocations as red bars in the trace UI without writing custom attribute queries. - Tenancy stamping (
org_id,workspace_id) and entity stamping (entity_id,entity_type) added as top-level audit fields. Sourced fromFORGE_ORG_ID/FORGE_WORKSPACE_ID/FORGE_AGENT_ID(orforge.yaml'sagent_id) with per-requestX-Forge-Org-ID/X-Forge-Workspace-IDheaders overriding the tenancy stamp. Field names match the guardrails library's MongoDBGuardrailAuditEventcolumns 1:1. - OpenTelemetry span content capture opt-in (issue #130). Prompts and completions stamp
gen_ai.input.messages+gen_ai.output.messages(current OTel GenAI semantic conventions, not the deprecated flat-string keys); tool calls stampforge.tool.args+forge.tool.result. Capture is off by default; redaction is on by default when capture is enabled. - Go library modules published —
forge-core,forge-skills,forge-pluginsare now importable via path-prefixed tags (forge-core/v0.15.0, etc.). The Initializ platform and other embedders can pull just the runtime, registry, or channel layers without the CLI.
What's new
Kubernetes-native scheduler (#162)
The default auto backend resolves to file when running on a laptop or CI and to Kubernetes when the agent is running as a pod. The K8s backend persists schedules as CronJob resources keyed on forge.agent.id / forge.schedule.id labels; the cluster's controller handles timing, etcd provides durability, and concurrencyPolicy: Forbid enforces overlap prevention natively.
# forge.yaml
schedules:
- id: daily-summary
cron: "0 9 * * *"
task: "Generate the daily ops summary and post to #ops"
channel: slack
channel_target: "C12345"
scheduler:
backend: auto
kubernetes:
namespace: "" # defaults to the agent pod's own namespace
service_url: "" # CronJob trigger pods POST here (in-cluster Service DNS)
allow_dynamic: false # whether the LLM (schedule_set) can create CronJobs
trigger_image: "" # default: curlimages/curl:8.10.1
auth_secret_name: "" # default: <agent_id>-internal-tokenEnd-to-end deploy:
forge build && forge package # emits k8s/cronjob-*.yaml + Secret template + RBAC
forge auth mint-token > /dev/null # first-deploy bootstrap
forge auth secret-yaml | kubectl apply -f - # populates the Secret out-of-band
kubectl apply -k .forge-output/k8s/ # brings up the agent + CronJobsSync() reconciles cluster CronJob resources against the declared yaml entries: creates missing, updates on spec drift, prunes dropped yaml entries, and preserves LLM-sourced (forge.schedule.source=llm) entries unconditionally. AllowDynamic: false (the default) keeps the LLM from creating CronJob resources at runtime; forge package materializes the yaml entries at build time instead. The RBAC Role forge package emits grants get / list / watch by default, with create / update / delete only when allow_dynamic: true.
Reference: docs/deployment/scheduler-kubernetes.md, docs/core-concepts/scheduling.md.
Guardrails audit emission and OTel span parity (#155, #159, #161)
Every mask / block / warn decision now emits a guardrail_check audit event on the configured sink (stderr, Unix socket, HTTP fallback) carrying the library gate, decision, guardrail, category, violation_count, and optional tool. With FORGE_GUARDRAIL_CAPTURE_EVIDENCE=true, the post-mask content lands in fields.evidence (or the pre-mask original on block / warn), scrubbed through a vendor-secret redact pass with a 4 KiB byte cap.
OpenTelemetry receives the same information as guardrail.<gate> child spans with forge.guardrail.gate / decision / type / category / violation_count attributes. Block decisions stamp OTel Error status with the violation summary — operators get red bars in the trace UI without custom attribute queries.
The direction field shipped in earlier drafts is replaced by gate (sourced from Result.Gate), matching the library vocabulary 1:1.
{
"ts": "2026-06-15T10:00:00Z",
"event": "guardrail_check",
"schema_version": "1.0",
"correlation_id": "fd111edd27c20101",
"task_id": "slack-...",
"fields": {
"gate": "input",
"decision": "masked",
"guardrail": "pii",
"category": "ssn",
"violation_count": 1
}
}Reference: docs/security/guardrails.md, docs/security/audit-logging.md.
Tenancy and entity stamping (#157, #164)
Audit events now carry top-level org_id, workspace_id, entity_id, and entity_type fields. SIEM consumers filter the audit stream by (org_id, workspace_id, entity_id) as a complete deploy identifier without joining against auth_verify rows. Field names match the guardrails library's MongoDB GuardrailAuditEvent columns 1:1, so consumers reading both streams join on the same column names.
| Field | Source | Per-request header override |
|---|---|---|
org_id |
FORGE_ORG_ID |
X-Forge-Org-ID |
workspace_id |
FORGE_WORKSPACE_ID |
X-Forge-Workspace-ID |
entity_id |
FORGE_AGENT_ID or forge.yaml agent_id |
— |
entity_type |
hardcoded "agent" (future-extensible to workflow / assistant) |
— |
Reference: docs/security/tenancy.md.
OpenTelemetry content capture and current GenAI semantic conventions (#130)
The observability.tracing.capture_content and observability.tracing.redact config knobs are now wired through Phase 3 span instrumentation. With capture on, the executor stamps gen_ai.input.messages (structured JSON array per current OTel GenAI semconv, superseding the deprecated gen_ai.prompt) and gen_ai.output.messages on llm.completion spans, plus forge.tool.args and forge.tool.result on tool.<name> spans.
Captured values pass through a vendor-secret redactor (Anthropic, OpenAI, GitHub, AWS, Slack, RSA/EC/OpenSSH/Private keys, Telegram bot tokens) and a 4 KiB byte cap with a …[truncated:N] marker byte-identical to the audit payload-capture marker — operators grepping [truncated: across spans and audit rows see aligned output.
Library modules
forge-core, forge-skills, and forge-plugins are now importable Go modules:
import (
"github.com/initializ/forge/forge-core/runtime"
"github.com/initializ/forge/forge-skills/parser"
"github.com/initializ/forge/forge-plugins/channels/slack"
)Tagged independently via path-prefixed tags: forge-core/v0.15.0, forge-skills/v0.15.0, forge-plugins/v0.15.0. The Initializ platform and other embedders pull just the runtime, registry, or channel layers without the CLI binary.
Reference: docs/reference/library-modules.md.
CLI
forge auth show-token— print the runtime token at<root>/.forge/runtime.tokenforge auth mint-token— generate, store, and print a fresh token (first-deploy bootstrap)forge auth secret-yaml— print a Kubernetes Secret manifest holding the token, ready forkubectl apply -f -forge build --policy <path>— override the security analyzer policy at build timeforge package— now also emitsk8s/cronjob-<id>.yaml,k8s/internal-token-secret.yaml,k8s/scheduler-role.yaml,k8s/scheduler-rolebinding.yamlwhenforge.yaml schedules[]is populated
forge.yaml
- New top-level
schedulerblock (backend,kubernetes.namespace,kubernetes.service_url,kubernetes.allow_dynamic,kubernetes.trigger_image,kubernetes.auth_secret_name) - New top-level
security.policy_pathfor the security analyzer override
Fixed
forge buildno longer fails the bundledcode-reviewskill on a fresh agent (#145). The security analyzer'strustedDomainsmap now includes GitHub-owned content endpoints (raw.githubusercontent.com,patch-diff.githubusercontent.com,gist.githubusercontent.com,objects.githubusercontent.com) andchatgpt.com. The env category score is capped at 25 points so multi-purpose skills declaring many config knobs aren't penalized linearly.DefaultPolicy.MaxRiskScoreraised from 75 → 90 so vetted bundled skills clear the default; operators wanting a stricter posture can lower the ceiling viasecurity.policy_pathorforge build --policy <path>.forge buildno longer leaks deadcompiled/artifacts into the agent image, and the Dockerfile context is tighter (#147). Thecompiled/skills.jsonandcompiled/prompt.txtfiles generated by the old skills stage are no longer written; the integration test asserts their absence. The.dockerignorenow excludesk8s/,build-manifest.json,compiled/,Dockerfile,.dockerignore, and.local-bins/. Thechecksums.jsonpath mismatch the runner experienced when the WorkDir was the.forge-output/directory is fixed by a fallback probe.forge buildno longer drops runtime apt-installed binaries from the final image (#149). The bins stage was unconditionally copying/usr/local/bin/from the bins stage to the runtime stage, silently overwriting any apt packages installed in the previous layer. Replaced with per-binaryCOPYdirectives that target only the bins the agent declared inrequires.bins.- Mask-decision evidence no longer leaks the pre-mask raw PII in the audit stream. The
guardrail_checkevent'sfields.evidencecarries the post-mask content fordecision: masked(the same payload the LLM saw downstream);decision: warned/decision: blockedcarry the original triggering text because the library produces no masked variant in those paths. - Pinned
k8s.io/client-goto v0.34.1 so the Go directive stays on 1.25 — v0.36 requires Go 1.26 which broke CI compilation.
Audit-event schema
AuditSchemaVersion remains "1.0". The new top-level fields (org_id, workspace_id, entity_id, entity_type) and the new event (guardrail_check) are additive — consumers ignoring unknown keys continue to work unchanged.
Documentation
docs/deployment/scheduler-kubernetes.md— new operator reference for the hybrid scheduler backenddocs/security/tenancy.md— tenancy + entity stamping precedence and per-request override headersdocs/security/guardrails.md— fullStructuredGuardrailsJSON schema, the source-precedence ladder forguardrails.jsonvsFORGE_GUARDRAILS_DB, evidence-capture posturedocs/reference/library-modules.md—forge-core/forge-skills/forge-pluginsimport paths, tag scheme, embedder APIdocs/core-concepts/binary-dependencies.md— the bin registry, resolution order, install methods, how to add a binarydocs/core-concepts/observability-tracing.md— Guardrail spans section added
Closed issues
- #130 — OpenTelemetry span content capture (
capture_content+redact) - #145 — security analyzer false positives on bundled
code-reviewskill - #147 — dead
compiled/artifacts and Dockerfile context bloat - #149 — bins stage wholesale
/usr/local/bin/copy - #155 —
guardrail_checkaudit emission - #157 — tenancy stamping (
org_id/workspace_id) - #159 — wire all five library gates + replace
directionwithgate - #161 — OpenTelemetry guardrail spans symmetric to audit
- #162 — hybrid scheduler backend (file + Kubernetes) end-to-end
- #164 —
entity_id+entity_typeaudit stamping
Upgrading from v0.14.x
Forge v0.15.0 is backwards-compatible with v0.14.x deployments. The new audit fields (org_id, workspace_id, entity_id, entity_type, the guardrail_check event) are additive — deployments that don't set the corresponding env vars keep emitting the pre-v0.15 JSON shape verbatim.
To opt into Kubernetes-native scheduling on an existing deployment:
- Add the
schedulerblock toforge.yaml(or rely on theautodefault — Forge auto-detects the in-cluster ServiceAccount token). - Re-run
forge package. The newk8s/cronjob-*.yaml,k8s/internal-token-secret.yaml,k8s/scheduler-role.yaml, andk8s/scheduler-rolebinding.yamlfiles appear in the output directory. - Populate the Secret out-of-band via
forge auth secret-yaml | kubectl apply -f -(or your ExternalSecrets / Sealed Secrets pipeline). kubectl apply -k .forge-output/k8s/.
The file backend remains the default outside of clusters — no change required for forge run on a laptop or CI.
Full changelog
git log v0.14.1..v0.15.0 --oneline in this repository, or browse v0.14.1...v0.15.0 on GitHub.