Skip to content

v0.15.0

Choose a tag to compare

@initializ-mk initializ-mk released this 15 Jun 20:48

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.Backend interface with two implementations: a file-backed scheduler (the existing 30s ticker + markdown persistence, unchanged behavior) and a Kubernetes backend using k8s.io/client-go for native CronJob CRUD. Selected at startup via scheduler.backend: auto | file | kubernetes in forge.yaml. In-cluster detection probes the projected ServiceAccount token at /var/run/secrets/kubernetes.io/serviceaccount/token.
  • forge package Kubernetes manifest generation. For every forge.yaml schedules[] entry, the build pipeline emits one cronjob-<id>.yaml, a credential-less Secret template, and a Role/RoleBinding scoped to the agent's namespace. The Secret manifest deliberately ships without a data field — the credential never lands in version control; operators populate via forge auth secret-yaml | kubectl apply -f -, ExternalSecrets, Sealed Secrets, SOPS, or Vault Agent Injector.
  • forge auth subcommand (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 scheduler CronJob trigger pods.
  • guardrail_check audit event emitted at all five library gates (input, context, tool_call, output, stream) with opt-in evidence capture controlled by FORGE_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 OTel Error status 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 from FORGE_ORG_ID / FORGE_WORKSPACE_ID / FORGE_AGENT_ID (or forge.yaml's agent_id) with per-request X-Forge-Org-ID / X-Forge-Workspace-ID headers overriding the tenancy stamp. Field names match the guardrails library's MongoDB GuardrailAuditEvent columns 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 stamp forge.tool.args + forge.tool.result. Capture is off by default; redaction is on by default when capture is enabled.
  • Go library modules publishedforge-core, forge-skills, forge-plugins are 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-token

End-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 + CronJobs

Sync() 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.token
  • forge 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 for kubectl apply -f -
  • forge build --policy <path> — override the security analyzer policy at build time
  • forge package — now also emits k8s/cronjob-<id>.yaml, k8s/internal-token-secret.yaml, k8s/scheduler-role.yaml, k8s/scheduler-rolebinding.yaml when forge.yaml schedules[] is populated

forge.yaml

  • New top-level scheduler block (backend, kubernetes.namespace, kubernetes.service_url, kubernetes.allow_dynamic, kubernetes.trigger_image, kubernetes.auth_secret_name)
  • New top-level security.policy_path for the security analyzer override

Fixed

  • forge build no longer fails the bundled code-review skill on a fresh agent (#145). The security analyzer's trustedDomains map now includes GitHub-owned content endpoints (raw.githubusercontent.com, patch-diff.githubusercontent.com, gist.githubusercontent.com, objects.githubusercontent.com) and chatgpt.com. The env category score is capped at 25 points so multi-purpose skills declaring many config knobs aren't penalized linearly. DefaultPolicy.MaxRiskScore raised from 75 → 90 so vetted bundled skills clear the default; operators wanting a stricter posture can lower the ceiling via security.policy_path or forge build --policy <path>.
  • forge build no longer leaks dead compiled/ artifacts into the agent image, and the Dockerfile context is tighter (#147). The compiled/skills.json and compiled/prompt.txt files generated by the old skills stage are no longer written; the integration test asserts their absence. The .dockerignore now excludes k8s/, build-manifest.json, compiled/, Dockerfile, .dockerignore, and .local-bins/. The checksums.json path mismatch the runner experienced when the WorkDir was the .forge-output/ directory is fixed by a fallback probe.
  • forge build no 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-binary COPY directives that target only the bins the agent declared in requires.bins.
  • Mask-decision evidence no longer leaks the pre-mask raw PII in the audit stream. The guardrail_check event's fields.evidence carries the post-mask content for decision: masked (the same payload the LLM saw downstream); decision: warned / decision: blocked carry the original triggering text because the library produces no masked variant in those paths.
  • Pinned k8s.io/client-go to 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

Closed issues

  • #130 — OpenTelemetry span content capture (capture_content + redact)
  • #145 — security analyzer false positives on bundled code-review skill
  • #147 — dead compiled/ artifacts and Dockerfile context bloat
  • #149 — bins stage wholesale /usr/local/bin/ copy
  • #155guardrail_check audit emission
  • #157 — tenancy stamping (org_id / workspace_id)
  • #159 — wire all five library gates + replace direction with gate
  • #161 — OpenTelemetry guardrail spans symmetric to audit
  • #162 — hybrid scheduler backend (file + Kubernetes) end-to-end
  • #164entity_id + entity_type audit 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:

  1. Add the scheduler block to forge.yaml (or rely on the auto default — Forge auto-detects the in-cluster ServiceAccount token).
  2. Re-run forge package. The new k8s/cronjob-*.yaml, k8s/internal-token-secret.yaml, k8s/scheduler-role.yaml, and k8s/scheduler-rolebinding.yaml files appear in the output directory.
  3. Populate the Secret out-of-band via forge auth secret-yaml | kubectl apply -f - (or your ExternalSecrets / Sealed Secrets pipeline).
  4. 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.