Skip to content

[Skill]: network-crm — channel-agnostic networking CRM (MongoDB-backed, exact-time follow-ups) #437

Description

@ratnesh-maurya

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 frontmatterrequired 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_tcpdeployed 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 schedulesremoves 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 scriptsblocks 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions