Skill name
network-crm
Description
A channel-agnostic personal networking CRM. A user drops a raw note after a meetup or coffee chat; the agent parses it into a structured contact record, persists it, schedules a follow-up reminder at an exact time, and delivers a ready-to-send draft message back to the same channel it came from.
The skill never names a platform. Inbound normalization already collapses Slack / Telegram / MS Teams into one ChannelEvent (forge-core/channels/plugin.go:45), and session continuity is already per-user via taskID = <channel>-<workspace>-<user> (forge-cli/channels/router.go:60), so the same SKILL.md works on any adapter with a one-line channels: change.
Target user: students and early-career folks who collect contacts at events and lose them because a spreadsheet CRM is too much admin.
Execution type
Script-backed (shell scripts in scripts/)
Category
productivity
Proposed tools
| Tool |
Script |
Purpose |
contact_log |
scripts/contact-log.sh |
Upsert a contact record (name, role, company, topics, tags, venue, met_at) |
contact_search |
scripts/contact-search.sh |
Query by name / company / tag / topic |
contact_due |
scripts/contact-due.sh |
List contacts whose followup_due has passed |
contact_touch |
scripts/contact-touch.sh |
Append an interaction to the log, bump followup_due, clear schedule_id |
Note parsing (raw text → structured fields) and draft generation are LLM work in the SKILL.md body — no tool needed for either.
Requirements
Binaries: mongosh, jq
- mongosh is ALREADY in the embedded bin registry
(forge-skills/registry/image-registry.yaml:147 — custom-run install, default_version 2.3.0),
so `forge build` installs it only when this skill is in the project.
Env vars: MONGODB_URI (required)
- Per-skill env isolation already holds: skill script env is built per skill entry
(forge-cli/runtime/runner.go:3779-3788) on a PATH+HOME-only base
(forge-cli/tools/exec.go:101). Other skills' scripts cannot read the URI.
Egress (HTTP): *.<cluster>.mongodb.net (skill frontmatter `egress_domains`)
Egress (raw TCP): *.<cluster>.mongodb.net:27017
- MUST go in forge.yaml `egress.allowed_tcp` — see enhancement (1). The SOCKS5
listener binds only when allowed_tcp is non-empty
(forge-cli/runtime/runner.go:798, forge-core/security/egress_proxy.go:22).
Storage is MongoDB (collection contacts, indexes on {owner, followup_due} + a text index). Raw-TCP egress through the existing SOCKS5 gate (#337) is the supported path; MongoDB is already named in docs/security/egress-control.md:140. MCP is not an option here — transport is http only and the official MongoDB MCP server is stdio (forge-core/types/config.go:600).
Example use case
student › Met Rahul at the GDG meetup. Senior Backend SDE at Swiggy.
Discussed Elixir concurrency and Redis caching. Ping me in 5 days at 6:30pm.
agent ▸ contact_log {"name":"Rahul","role":"Senior Backend SDE","company":"Swiggy",
"topics":["Elixir concurrency","Redis caching"],
"tags":["GDG","Backend"],"venue":"GDG meetup",
"followup_due":"2026-09-07T18:30:00+05:30"}
▸ schedule_set {"id":"followup-rahul-swiggy","cron":"30 18 7 9 *",
"task":"Read the contacts record for Rahul (Swiggy). Send a
follow-up draft about Elixir concurrency. Then call
schedule_delete with id followup-rahul-swiggy.",
"channel":"telegram","channel_target":"<chat id>"}
Logged Rahul — Senior Backend SDE @ Swiggy (#GDG #Backend).
Reminder set for Sep 7, 18:30. Draft ready:
"Hey Rahul, great connecting at GDG! I read up on those Elixir concurrency
points we discussed — really insightful perspective, thanks for sharing."
Arbitrary-time reminders work today with no core change: the 5-field cron parser supports day-of-month + month (forge-core/scheduler/parser.go:60-80), so an exact datetime is expressible; the schedule is made one-shot by having its own task text call schedule_delete (agent-created schedules are deletable — only forge.yaml ones are locked).
Security considerations
- Connection string is a per-skill required env var, invisible to every other skill's scripts (see Requirements above). Supply via the encrypted secret store or pod env, never in
forge.yaml.
- Per-skill
guardrails.deny_output redacts any mongodb(+srv)://… string that reaches model output.
- Raw-TCP reaches Mongo through
ValidateAndDial — the same allowlist + audit shape as HTTP egress, port-exact (:27018 denied before dial).
- All writes are scoped by an
owner field keyed to the channel user, so one deployment can serve many users without cross-reads — blocked today by enhancement (5).
- No destructive tool:
contact_touch appends, never deletes. No collection drop, no deleteMany.
Core enhancements this needs (in priority order)
(1) allowed_tcp in skill frontmatter — required for a self-contained DB-backed skill
Frontmatter has egress_domains and denied_tools only (forge-skills/contract/types.go:83). A DB skill therefore cannot declare its own port-scoped egress, and the SOCKS5 listener won't even bind without a forge.yaml entry. Proposal: add allowed_tcp: [] to SkillRequirements, aggregate it in forge-skills/requirements, and merge into security.Resolve alongside skill egress_domains in forge-cli/build/egress_stage.go. Same validation as the existing host:port matcher (forge-core/security/tcp_matcher.go).
(2) NetworkPolicy must consume allowed_tcp — deployed agents currently cannot reach any DB
forge-core/security/network_policy.go emits ports 443/80 only, and allowed_tcp is build-validated but deliberately not consumed by the artifacts (forge-cli/build/egress_stage.go:55). A packaged agent with allowed_tcp: [...:27017] ships a policy that blocks 27017 — the failure surfaces at first connect in-cluster, not at build. Proposal: emit an egress port per distinct allowed_tcp port.
(3) One-shot schedules — removes the self-delete workaround
forge-core/scheduler/parser.go:19 supports 5-field cron, aliases, and @every — nothing fires once. The dom+month workaround repeats annually and leans on the model reliably calling schedule_delete inside its own task text. Proposal: @at <RFC3339> (or once: true on schedule_set) with delete-after-fire in the store.
(4) Timezone for schedules
CronSchedule.Next uses t.Location() — process local time. A container defaults to UTC, so "6:30pm" lands hours off unless TZ is set on the pod. Proposal: a timezone field on ScheduleConfig / schedule_set, or at minimum document the TZ requirement in docs/core-concepts/scheduling.md.
(5) Channel user identity for skill scripts — blocks multi-user deployments
Skill scripts get PATH + HOME + declared vars only (forge-cli/tools/exec.go:101-107). X-Forge-Channel-User is set by the router (forge-cli/channels/router.go:106) but stops at the auth middleware (forge-core/auth/middleware.go:261). Session memory is already per-user, but a shared record store is not — there is no way for a script to key rows by owner. Proposal: inject FORGE_CHANNEL_USER (and optionally FORGE_CHANNEL_EMAIL) into SkillCommandExecutor env, opt-in per skill.
(6) In-container raw-TCP posture needs a decision or a doc
The local egress proxy is not started in a container unless browser tools are active (forge-cli/runtime/runner.go:790), so ALL_PROXY is absent in the deployed pod and the DB dial is unproxied — enforcement silently shifts to the NetworkPolicy, which (2) says doesn't cover the port. Either start the SOCKS5 listener in-container when allowed_tcp is set, or state plainly in docs/security/egress-control.md that the raw-TCP gate is a local-run mechanism.
(7) Docs: mongosh SOCKS5 row is stale
docs/security/egress-control.md:192 lists mongosh under "Native SOCKS5? No" and routes readers to proxychains-ng — which the same page notes is broken on macOS (SIP) and for static binaries. mongosh 2.x supports SOCKS5 natively via the proxyHost / proxyPort connection-string options, which is the correct guidance for the registry-installed v2.3.0.
(8) Nice-to-have: inbound voice notes
The original ask was voice-message capture. ChannelEvent.Attachments exists (forge-core/channels/plugin.go:59) but no adapter populates it — Telegram reads message.Text only (forge-plugins/channels/telegram/telegram.go:321), so a voice note arrives as an empty message. Needs adapter-side mapping of voice/audio to Attachment{URL, MimeType}, plus a transcription tool. Out of scope for v1 (text-only), tracked here for context.
Items (1) and (2) are the two that generalize well beyond this skill — together they make any database-backed skill declarative and deployable, which today it isn't.
Skill name
network-crmDescription
A channel-agnostic personal networking CRM. A user drops a raw note after a meetup or coffee chat; the agent parses it into a structured contact record, persists it, schedules a follow-up reminder at an exact time, and delivers a ready-to-send draft message back to the same channel it came from.
The skill never names a platform. Inbound normalization already collapses Slack / Telegram / MS Teams into one
ChannelEvent(forge-core/channels/plugin.go:45), and session continuity is already per-user viataskID = <channel>-<workspace>-<user>(forge-cli/channels/router.go:60), so the same SKILL.md works on any adapter with a one-linechannels:change.Target user: students and early-career folks who collect contacts at events and lose them because a spreadsheet CRM is too much admin.
Execution type
Script-backed (shell scripts in
scripts/)Category
productivityProposed tools
contact_logscripts/contact-log.shcontact_searchscripts/contact-search.shcontact_duescripts/contact-due.shfollowup_duehas passedcontact_touchscripts/contact-touch.shfollowup_due, clearschedule_idNote parsing (raw text → structured fields) and draft generation are LLM work in the SKILL.md body — no tool needed for either.
Requirements
Storage is MongoDB (collection
contacts, indexes on{owner, followup_due}+ a text index). Raw-TCP egress through the existing SOCKS5 gate (#337) is the supported path; MongoDB is already named indocs/security/egress-control.md:140. MCP is not an option here — transport ishttponly and the official MongoDB MCP server is stdio (forge-core/types/config.go:600).Example use case
Arbitrary-time reminders work today with no core change: the 5-field cron parser supports day-of-month + month (
forge-core/scheduler/parser.go:60-80), so an exact datetime is expressible; the schedule is made one-shot by having its own task text callschedule_delete(agent-created schedules are deletable — only forge.yaml ones are locked).Security considerations
forge.yaml.guardrails.deny_outputredacts anymongodb(+srv)://…string that reaches model output.ValidateAndDial— the same allowlist + audit shape as HTTP egress, port-exact (:27018denied before dial).ownerfield keyed to the channel user, so one deployment can serve many users without cross-reads — blocked today by enhancement (5).contact_touchappends, never deletes. No collection drop, nodeleteMany.Core enhancements this needs (in priority order)
(1)
allowed_tcpin skill frontmatter — required for a self-contained DB-backed skillFrontmatter has
egress_domainsanddenied_toolsonly (forge-skills/contract/types.go:83). A DB skill therefore cannot declare its own port-scoped egress, and the SOCKS5 listener won't even bind without aforge.yamlentry. Proposal: addallowed_tcp: []toSkillRequirements, aggregate it inforge-skills/requirements, and merge intosecurity.Resolvealongside skillegress_domainsinforge-cli/build/egress_stage.go. Same validation as the existinghost:portmatcher (forge-core/security/tcp_matcher.go).(2) NetworkPolicy must consume
allowed_tcp— deployed agents currently cannot reach any DBforge-core/security/network_policy.goemits ports 443/80 only, andallowed_tcpis build-validated but deliberately not consumed by the artifacts (forge-cli/build/egress_stage.go:55). A packaged agent withallowed_tcp: [...:27017]ships a policy that blocks 27017 — the failure surfaces at first connect in-cluster, not at build. Proposal: emit an egress port per distinctallowed_tcpport.(3) One-shot schedules — removes the self-delete workaround
forge-core/scheduler/parser.go:19supports 5-field cron, aliases, and@every— nothing fires once. The dom+month workaround repeats annually and leans on the model reliably callingschedule_deleteinside its own task text. Proposal:@at <RFC3339>(oronce: trueonschedule_set) with delete-after-fire in the store.(4) Timezone for schedules
CronSchedule.Nextusest.Location()— process local time. A container defaults to UTC, so "6:30pm" lands hours off unlessTZis set on the pod. Proposal: atimezonefield onScheduleConfig/schedule_set, or at minimum document theTZrequirement indocs/core-concepts/scheduling.md.(5) Channel user identity for skill scripts — blocks multi-user deployments
Skill scripts get
PATH+HOME+ declared vars only (forge-cli/tools/exec.go:101-107).X-Forge-Channel-Useris set by the router (forge-cli/channels/router.go:106) but stops at the auth middleware (forge-core/auth/middleware.go:261). Session memory is already per-user, but a shared record store is not — there is no way for a script to key rows by owner. Proposal: injectFORGE_CHANNEL_USER(and optionallyFORGE_CHANNEL_EMAIL) intoSkillCommandExecutorenv, opt-in per skill.(6) In-container raw-TCP posture needs a decision or a doc
The local egress proxy is not started in a container unless browser tools are active (
forge-cli/runtime/runner.go:790), soALL_PROXYis absent in the deployed pod and the DB dial is unproxied — enforcement silently shifts to the NetworkPolicy, which (2) says doesn't cover the port. Either start the SOCKS5 listener in-container whenallowed_tcpis set, or state plainly indocs/security/egress-control.mdthat the raw-TCP gate is a local-run mechanism.(7) Docs: mongosh SOCKS5 row is stale
docs/security/egress-control.md:192listsmongoshunder "Native SOCKS5? No" and routes readers toproxychains-ng— which the same page notes is broken on macOS (SIP) and for static binaries. mongosh 2.x supports SOCKS5 natively via theproxyHost/proxyPortconnection-string options, which is the correct guidance for the registry-installed v2.3.0.(8) Nice-to-have: inbound voice notes
The original ask was voice-message capture.
ChannelEvent.Attachmentsexists (forge-core/channels/plugin.go:59) but no adapter populates it — Telegram readsmessage.Textonly (forge-plugins/channels/telegram/telegram.go:321), so a voice note arrives as an empty message. Needs adapter-side mapping ofvoice/audiotoAttachment{URL, MimeType}, plus a transcription tool. Out of scope for v1 (text-only), tracked here for context.Items (1) and (2) are the two that generalize well beyond this skill — together they make any database-backed skill declarative and deployable, which today it isn't.