v0.10.0
Forge v0.10.0 — Dynamic Secret Overlay, Configurable Security Scoring, K8s Channel Manifests, Slack Bot Admission
Release Date: May 18, 2026
Full Changelog: v0.9.2...v0.10.0
Forge v0.10.0 makes encrypted secrets, security policy, channel deployment, and inter-bot communication first-class operator concerns. Skill-declared env vars now flow from the encrypted store to the agent process without code changes; security audits can be downgraded via YAML policy with full auditability; Kubernetes manifests finally include channel env vars from the same source as docker-compose; and Slack agents can now accept @-mentions from operator-allowlisted bots while staying immune to self-loops. Two latent bugs in the secret provider chain are also fixed. 62 files changed, 2,718 insertions, 728 deletions across forge-cli, forge-core, forge-plugins, and forge-skills.
Highlights
| # | Area | Change | Issue / PR |
|---|---|---|---|
| 1 | Secrets | Skill-declared env vars in encrypted store reach the agent's OS env via dynamic provider.List() discovery — no hardcoded key list |
#48 / #51 |
| 2 | Skills audit | New forge skills audit --policy <path> overrides scoring and policy checks; downgrades carry (via policy) / (acknowledged by policy) annotations |
#49 / #53 |
| 3 | K8s + channels | Channel env vars from <channel>-config.yaml now flow into k8s/secrets.yaml, k8s/deployment.yaml, and docker-compose.yaml from a single canonical source |
#50 / #54 |
| 4 | Secrets | Stale global ~/.forge/secrets.enc with a different passphrase no longer poisons the chain — eager-load validation drops it with a clear warning |
#52 / #57 |
| 5 | Slack | New allow_bot_ids setting admits @-mentions from operator-trusted bots; the agent's own bot_id is unconditionally dropped (self-loop guard) |
#55 / #56 |
What's New
Dynamic Secret Overlay for Skill-Declared Env Vars (PR #51, closes #48)
Before: the runtime overlaid only a hardcoded set of LLM/channel keys (OPENAI_API_KEY, SLACK_BOT_TOKEN, etc.) from the encrypted secrets store. A skill declaring metadata.forge.requires.env.required: [ACME_API_TOKEN] would see an empty value at runtime, even after forge secret set ACME_API_TOKEN ....
Now: a new secretOverlayKeys(provider) helper unions the builtin set with every key the provider exposes via Provider.List(). Custom skill-declared keys flow through the encrypted store to the OS env automatically — no code change required when adding a new skill.
# skills/my-skill/SKILL.md
metadata:
forge:
requires:
env:
required: [ACME_API_TOKEN]forge secret --local set ACME_API_TOKEN my-secret-value
forge run # ACME_API_TOKEN is now in the agent's environmentCross-category secret-reuse detection continues to operate on the builtin set only — custom keys have no defined purpose category and are not part of the reuse check.
Policy-Configurable Security Scoring (PR #53, closes #49)
forge skills audit now accepts a --policy <path> flag that loads a YAML SecurityPolicy. The policy affects both policy-violation checks (existing) and scoring (new). Three new scoring overrides let operators down-weight builtin classifications:
# policy.yaml
script_policy: allow
max_risk_score: 90
trusted_domains: [internal.example.com] # +2 instead of +10 (unknown)
acknowledged_bins: [python] # +3 instead of +15 (builtin high-risk)
acknowledged_env: [DB_PASSWORD] # +5 instead of +10 (builtin sensitive)forge skills audit --dir skills --policy policy.yamlEvery override is auditable. Affected factors carry (via policy) or (acknowledged by policy) in their description, visible in both --format text and --format json:
egress +2 trusted domain (via policy): internal.example.com
binary +3 high-risk binary (acknowledged by policy): python
env +5 sensitive variable (acknowledged by policy): DB_PASSWORD
Scoring overrides can only down-weight builtin classifications — a binary not in the high-risk set stays at +3 even if listed in acknowledged_bins. This prevents accidental score inflation.
API change: AnalyzeSkillDescriptor and AnalyzeSkillEntry now take a SecurityPolicy parameter. Pass SecurityPolicy{} to reproduce historical default scoring exactly.
Channel Env Vars in Kubernetes Manifests (PR #54, closes #50)
Before this release, forge build generated k8s/deployment.yaml and k8s/secrets.yaml containing only skill-declared env vars. Channel env vars (SLACK_BOT_TOKEN, TELEGRAM_BOT_TOKEN, etc.) were silently dropped — even though forge package --with-channels injected them into docker-compose via a hardcoded switch statement. The two output paths disagreed from the same forge.yaml.
A new ChannelsStage (forge-cli/build/channels_stage.go) reads each <channel>-config.yaml and unions every _env-suffixed setting value into Spec.Requirements.EnvRequired. EnvVarsFromConfig in forge-cli/channels/env.go is the canonical helper; generateDockerCompose in cmd/package.go calls the same function, so both output paths produce a consistent env-var set.
# slack-config.yaml — the canonical source
adapter: slack
settings:
app_token_env: SLACK_APP_TOKEN
bot_token_env: SLACK_BOT_TOKEN
custom_env: MY_PROJECT_SLACK_OVERRIDE # ← appears in K8s + docker-composeAfter forge build:
# .forge-output/k8s/secrets.yaml
apiVersion: v1
kind: Secret
metadata:
name: my-agent-secrets
stringData:
SLACK_APP_TOKEN: ""
SLACK_BOT_TOKEN: ""
MY_PROJECT_SLACK_OVERRIDE: ""
TELEGRAM_BOT_TOKEN: ""Adding a new channel adapter requires zero build-pipeline edits. Operators (and channel-adapter authors) just declare env vars via the _env suffix convention in their channel YAML.
Behavior change worth calling out: SLACK_SIGNING_SECRET is no longer injected. The Slack adapter is Socket Mode and never reads it; the previous hardcoded map included it erroneously. Operators using a custom Slack adapter that does need it can add signing_secret_env: SLACK_SIGNING_SECRET to their slack-config.yaml.
Slack Bot @-Mention Admission with Self-Loop Guard (PR #56, closes #55)
The Slack adapter previously dropped every event whose bot_id was non-empty — silently ignoring @-mentions from scheduler bots, monitoring tools, workflow steps, and CI bots even when an operator explicitly wired them up.
A new optional allow_bot_ids setting admits specific bots:
# slack-config.yaml
adapter: slack
settings:
app_token_env: SLACK_APP_TOKEN
bot_token_env: SLACK_BOT_TOKEN
allow_bot_ids: B0123ABC,B0456DEF # comma-separated bot_idsThree coordinated rules keep loops bounded:
| Rule | Behavior |
|---|---|
| Self-loop guard (always on) | The agent's own bot_id is dropped before the allowlist is consulted. No opt-out — even if its own ID is misconfigured into allow_bot_ids, the guard short-circuits. |
| Allowlist gate (opt-in) | Bots not in allow_bot_ids stay dropped. Empty/absent setting preserves today's behavior (no other bots admitted). |
| Mention requirement (unchanged) | Admitted bots still must include <@FORGE_AGENT> in the message text. Chatter without a tag is ignored. |
resolveBotID now captures both user_id and bot_id from Slack's auth.test response — user_id drives mention matching; bot_id powers the self-loop guard. Drop reasons are emitted to stderr with the bot_id and a pointer to slack-config.yaml allow_bot_ids so operators can self-diagnose.
Stale Global Secrets File No Longer Poisons the Chain (PR #57, closes #52)
A latent bug in OverlaySecretsToEnv and Runner.buildSecretProvider — exposed during the validation of #51 — caused a stale ~/.forge/secrets.enc from an unrelated project (encrypted with a different passphrase) to break ChainProvider.List() for everyone. Local file's keys would silently disappear from the agent's env.
A new viableEncryptedFileProviders helper eagerly validates each candidate file at chain-build time:
| Candidate state | Behavior |
|---|---|
File missing (os.IsNotExist) |
Silently skipped — common case, no log noise |
| Decryption succeeds | Admitted; cleartext is cached for subsequent reads (no extra Argon2id) |
| Decryption fails (wrong passphrase, corruption) | Dropped from the chain with a clear warning naming the file path |
forge: skipping secrets provider that failed to load
(label=global, path=/Users/.../.forge/secrets.enc,
error=decrypting secrets file: ... wrong passphrase?)
ChainProvider semantics stay strict — non-NotFound errors from Get / List still propagate. The fix is at the chain-construction site: we don't admit providers we already know will fail.
Documentation
Each PR shipped with synced docs. Notable additions:
- docs/security/secret-management.md — new
Skill-Declared SecretsandProvider Chain Validationsections. - docs/skills/skills-cli.md — new
Security Auditsection with the policy YAML schema and the(via policy)annotation contract. - docs/deployment/kubernetes.md — new
Env Var Injectionsection documenting the union of skill + channel env vars. - docs/core-concepts/channels.md — new
Bot Authorship Admissionsection. - docs/core-concepts/runtime-engine.md —
Secret Overlay and Reuse Detectionextended with the dynamic overlay union and chain validation.
Upgrade Notes
For most users this is a drop-in upgrade — no forge.yaml changes needed.
forge-skills/analyzerAPI:AnalyzeSkillDescriptorandAnalyzeSkillEntrynow take a thirdSecurityPolicyparameter. PassSecurityPolicy{}for the historical default. Affects only direct importers of the analyzer package.- Slack docker-compose:
SLACK_SIGNING_SECRETis no longer auto-injected. If you depended on it (you likely didn't — Socket Mode never reads it), addsigning_secret_env: SLACK_SIGNING_SECRETto yourslack-config.yaml. - Skill audit CLI:
forge skills audit --dir <path>is required for subdirectory-style skills (skills/<name>/SKILL.md). The default still targets a flatSKILL.md.
Install
# Homebrew
brew upgrade initializ/tap/forge
# One-line install/upgrade
curl -sSL https://raw.githubusercontent.com/initializ/forge/main/install.sh | bash
# Docker
docker pull ghcr.io/initializ/forge:v0.10.0
docker pull ghcr.io/initializ/forge:latestContributors
Thanks to the operators who reported the channel env / Slack bot / encrypted secrets issues that drove this release.