Skip to content

Franco 0.4.0

Choose a tag to compare

@github-actions github-actions released this 17 Oct 00:39
· 11 commits to main since this release
4af0db1

Franco is now open source under GortexAI/franco, published as a container image, and released automatically.

Upgrading from 0.3.0

  • Set your postal address. The template franco.yaml now ships with compliance.require_footer: true and a blank postal_address. No cold email sends until you enter your own address. If your config predates this, the footer stays off until you turn it on.
  • The dashboard only answers on localhost. If you reach it under another hostname (from Docker, for example), list it in FRANCO_DASHBOARD_HOSTS.
  • New config sections (outreach, compliance) all have defaults. Existing configs load unchanged.

Added

  • Approved-copy outreach (outreach.mode: templates):
    • Signed-off sequences in outreach.campaigns are sent word for word to researched accounts, with no model involved.
    • Merge fields are limited to {{first_name}}, {{last_name}}, {{company}}, {{title}}, {{sender_name}} and {{verified_observation}}. The observation is a sourced, dated fact recorded per account. An account with an unfilled field is skipped, never sent with a gap.
    • Steps are scheduled in business days in outreach.timezone, and each follow-up is re-timed from the day the previous step actually went out.
  • Raad mode (outreach.mode: raad), named after Raad Mobrem, CEO of Intro, whose outbound approach it follows:
    • Up to three outreach.benefits; every funnel names exactly one.
    • outreach.funnels are narrow segments matched on observed signals (TECH_STACK=AdSense|Prebid) or account fields (segment=marketplace), tried narrowest first.
    • Email 1 is written from the funnel's story, the account's sourced facts, the one benefit and a role-specific angle, then the funnel's approved follow-ups are queued.
    • franco funnels (and the dashboard's Campaigns tab) shows how companies split across funnels; --company DOMAIN explains one placement.
    • The profiler now detects publisher ad tags (Google AdSense, Ad Manager, Prebid, Amazon Publisher Services).
  • Bring your own lists as CSV files. franco import FILE --kind accounts|contacts|suppressions, plus an Import CSV panel on the dashboard's Contacts tab:
    • Each kind has a template (--template), header aliases, --dry-run and optional --verify.
    • Every row is validated before anything is written.
    • franco suppress import parses real CSV (with a reason column, or one address per line), and export quotes properly.
  • Preview and sign-off. New approved-copy campaigns and raad funnels start in preview (outreach.preview_accounts, default 10 per owner):
    • Preview emails always wait for review, even on autopilot, and nothing sends until someone signs off.
    • Commands: franco preview [KEY] [--csv FILE], franco preview signoff KEY --by NAME, franco preview reopen KEY. The Outbox shows a sign-off card for each campaign in preview.
  • Compliance in code:
    • compliance.require_footer puts the sender, company and postal address plus "Reply "no thanks" to stop." on every cold email in every mode. The gate refuses cold email without the footer, and nothing sends while the postal address is blank.
    • Cold email gets List-Unsubscribe headers (mailto, plus optional one-click HTTPS).
    • compliance.blocked_claims is enforced on every email, replies included.
    • A shared suppression list covers addresses and whole domains (franco suppress).
  • Account-level outreach:
    • One contact per account in a sequence at a time, and one owner per account.
    • A reply or opt-out from anyone stops queued email to everyone at that company.
    • Follow-ups wait for the previous step, and are cancelled if it was rejected or failed.
  • Replies:
    • Quoted history and Franco's own footer are removed before any rule runs.
    • "no thanks" (body or subject) is an opt-out, and is suppressed before the optional acknowledgement is sent.
    • Out-of-office replies no longer end a sequence.
    • A new later intent pauses the contact and records when to get back in touch.
    • outreach.reply_templates provides approved wording per intent; not_interested sends its template as written.
  • Autopilot toggle. franco autopilot on|off|default (and an Outbox button) overrides channels.email.require_approval at runtime; turning it on approves what is waiting. Every gate still applies.
  • Outbox context and editing. Each draft shows why it exists: campaign, owner, buyer, observation with its source and the date checked. Drafts can be edited before approval with franco outbox --edit ID or E on the dashboard.
  • Container image ghcr.io/gortexai/franco, for linux/amd64 and linux/arm64, with an SBOM and a signed build-provenance attestation:
    • Pull requests build and smoke-test it.
    • Every push to main publishes :edge, and each release publishes :X.Y.Z, :X.Y and :latest.
    • docker-compose.yml uses the published image.
  • Release workflow. Tagging vX.Y.Z checks the version against pyproject.toml and the CHANGELOG, builds and tests the sdist and wheel, and creates the GitHub Release. See "Releasing" in CONTRIBUTING.md.
  • CI tests Python 3.11, 3.12, 3.13 and 3.14 on Linux, macOS and Windows.
  • SECURITY.md, CONTRIBUTING.md, CODE_OF_CONDUCT.md, issue forms and a pull request template.

Security

  • The dashboard refuses cross-site requests and DNS-rebound hosts. Any web page could previously POST to localhost:5555, for example to turn on autopilot.
  • Raad-mode drafts are checked in code. Facts reach the model as fenced, untrusted data. Code rejects any draft that contains a blocked claim, HTML, an email address, an IP address, or any link or domain other than the funnel's landing page (Unicode look-alike dots included).
  • Dependencies are locked and fully scanned. requirements.txt is generated by pip-compile from requirements.in, with every package pinned and hashed.
    • Dependabot alerts now cover all 28 packages, up from 12.
    • Dependabot security updates work again; they had been failing without a lockfile.
    • Renovate recompiles the lock and keeps pyproject.toml, requirements.in and the lock in step. tests/test_dependencies.py checks that they agree.
  • Every GitHub Action is pinned to a full commit SHA, with Renovate keeping the pins current, so a moved or hijacked tag can't change what runs with publishing and signing permissions.
  • Fixed all 32 CodeQL alerts inherited from the original code:
    • Path injection (4, high): the dashboard serves only files listed under franco/web/, never a requested path.
    • Clear-text logging of secrets (6, high): the setup wizard reads API keys and passwords through a separate hidden-input function. Secrets were only ever written to .env.
    • Bad HTML tag filter (1, high): script and style stripping handles end tags like </script >.
    • Stack-trace exposure (20, medium): dashboard endpoints and run reports log the details and show fixed wording. Franco's own explanations, such as "OpenStreetMap has no category for 'widgets'", are still shown as written.
    • Log injection (1, medium): newlines are removed from logged search terms, provider names and errors.
  • CSV exports (prospects, preview, suppressions) neutralise cells starting with =, +, - or @, so imported or scraped text can't run as a spreadsheet formula.
  • Scraped website and LinkedIn URLs are linked only when they are http(s).
  • Secret scanning with push protection, private vulnerability reporting, CodeQL and Code Quality are enabled on the repository.

Fixed

  • The Docker image never had the Claude CLI. Its installer is a bash script but ran under sh, and || true hid the failure, so containers couldn't make any model call. It now runs with bash, and the build fails if claude is missing.
  • A .dockerignore keeps .env, franco.local.yaml, data/ and the virtualenv out of local Docker builds.
  • When several steps of a sequence were overdue at once, they no longer all send in the same cycle.
  • Accounts staged on a weekend start on Monday, not Tuesday.
  • No empty campaign rows when every account in a batch is skipped.
  • A usage test no longer fails between 5pm and midnight Pacific (it seeded data with the UTC date while Franco uses the local one).