0.3.0 — safer defaults, and claims that have to prove themselves

0.3.0 is not a capability release. It narrows what the server exposes by default, turns consent and network access into things you grant rather than things you inherit, and takes back a set of claims the product was making without the evidence to back them. Six breaking changes, all of them defaults tightening. Here is what changed and what you need to do.

v0.3.019 tools advertised209 registered6 breaking changesegress off by default

Released 2026-08-07 · published 2026-08-10

In one line: the server now advertises 19 tools instead of 205 and routes the rest through sfi.run_analysis; live access needs a scoped, expiring, org-bound grant rather than a per-call flag; outbound network is off unless you ask for it; and a set of over-claims - a refresh that could delete a vault and report success, a coverage row that asserted a confirmed zero about an org with 4,296 reports, one validation rule counted as two blockers - were fixed and fenced off by a generated manifest that fails the build when the product's stated facts drift from its real registries.

What breaks when you upgrade

Every breaking change in 0.3.0 is a default getting stricter. None of them change a tool's inputs or outputs, and legacy response keys are untouched - so if you consume this server programmatically, your parsing is safe and your access is what needs attention.

What changedWhat you do
Default tool profile is core - 19 directly invokable schemas. A tools/call outside the advertised set is denied.Call non-core tools via sfi.run_analysis { name, args }, or set SFI_TOOL_PROFILE=full.
liveEnabled: true is no longer a live-access path. It is intent only, and ignored for access.Grant standing consent with sfi.live_consent, or set SFI_LIVE_PLANE_ENABLED=1.
v1 consent records are dropped. On-disk grants without a grantId / expiresAt stop working.Re-grant once: sfi.live_consent { grant: true }.
Grants expire and bind to an org. Default TTL is 7 days, bound to Salesforce OrgId + principal.Expect periodic re-grants. Re-pointing an alias at another org refuses until you re-grant.
The npm update check is opt-in. sfi mcp no longer contacts the registry by default.Set SFI_UPDATE_CHECK=1 or SFI_NETWORK_MODE=updates-only if you want it.
Success envelopes stamp contentPolicy - about 280 bytes marking org strings as untrusted data.Nothing, unless you assert on exact envelope shape in tests.

Why 205 advertised tools became 19

The tool count was a number this project used to quote with some pride, and advertising all of it was a mistake. An MCP host pays for every advertised schema on every turn: the list goes into context, and the host's routing gets harder as the list grows. Most of those 205 are specialist analyses that a general question should never be choosing between - the cost was paid constantly, the benefit arrived rarely.

So 0.3.0 splits registered from advertised. There are still 209 registered tools. The default core profile advertises a 19-tool spine - resolve and search, component and edge retrieval, impact, effective permissions, order of execution, org overview and history, health, routing and synthesis, the three discovery tools, and sfi.live_consent - and everything else is reached by name through one gateway:

the gateway
# before 0.3.0 — direct call, one of 205 advertised schemas
interpret(componentId: "CustomField:Account.Industry__c")

# 0.3.0 — same tool, same args, reached through the gateway
run_analysis(
  name: "sfi.interpret",
  args: {componentId: "CustomField:Account.Industry__c"}
)

# restore the old behaviour entirely
export SFI_TOOL_PROFILE=full

The gateway is a routing change, not an escape hatch: the target must be a registered tool and its args are validated against that tool's real schema. sfi.describe_analysis gained a progressive detail level (summary | schema | full, defaulting to summary under core) so a host can discover a tool's shape without paying for the full schema up front. The skills, agents and slash commands shipped with the package all teach the gateway, and a pnpm skill-gateway check fails CI if any of them go back to instructing a direct non-core call.

The opt-in live plane - the read-only SOQL surface that answers assignment questions an offline vault genuinely cannot - previously accepted liveEnabled: true on a call as a consent signal. That is a bad shape: the thing being authorised was also the thing asking for authorisation, on every call, with no record and no boundary.

Live consent is now a v2 grant. It binds to the Salesforce OrgId and principal (read via a read-only sf org display), carries scopes - aggregate, sample, users - expires after 7 days by default, and the grant id is disclosed on every live answer so an answer can be traced to the authorisation that permitted it. Sample and user-level reads require an explicit scope step-up rather than riding on a general grant. Re-pointing an alias at a different org refuses the grant instead of quietly answering about the new one.

The network is off unless you ask for it

"Offline by default" was true of the analysis path and not quite true of the process. The CLI phoned the npm registry for an update check on startup. That is a small thing, and it is also exactly the kind of small thing that makes a security review of an org-metadata tool go badly.

Outbound egress is now gated by SFI_NETWORK_MODE, defaulting to off, with updates-only and salesforce-read as the other two settings. sfi refresh and authorised live reads temporarily elevate to salesforce-read and drop back down. Runtime model download is always denied. One adapter in the core package is the choke point for all three callers - update check, Tooling API, and the live CLI/REST path - so the policy is enforced in one place rather than remembered in three.

The refresh that deleted a vault and reported success

This is the most serious bug this project has shipped, and it deserves to be described plainly.

reconcileSourceDeletions - the step that removes vault files for metadata deleted in the org - compared raw tree-relative paths. The authoritative retrieve lands files under <pkgDir>/main/default/<type>/…, while an older vault stores them flat as <type>/…. Those never match. So on a vault with the older layout, every in-scope file was classified as "deleted in the org" and removed, while byte-identical copies sat in the authoritative output directory.

Reproduced against a synthetic fixture by executing the shipped code: 8 of 8 files deleted. On a real org it destroyed 974 nodes - 8,641 down to 7,667, including 850 CustomFields - and reported status: success with exit code 0. It also recurred by construction: additive pulls (reports, dashboards, auto-expanded objects) write flat while the authoritative tree is nested, so every additively-pulled file was deleted by the next refresh, before the re-pull ran.

Two fixes, because one was not enough:

  • Path comparison is now layout-agnostic. The key is derived by probing for the shortest trailing path segments that still resolve to a ComponentType, so a future wrapper directory falls off by construction instead of needing another special case.
  • A wholesale-deletion guard. A reconcile that would remove every in-scope file while the retrieve did return files of those types - or more than half the considered set - now refuses, deletes nothing, and says why. A deletion set that large is far more likely to mean the layout changed than that the org dropped that much metadata. The refusal is printed to stdout and carried through the retrieve result, because a silent refusal recreates the same blind spot from the other direction.

Claims that had to be taken back

The rest of the fix list is one theme: the product asserting something it could not back. Each of these was individually small and collectively the thing that decides whether output like this is usable when a delete is on the line.

The claimThe reality
A coverage row read retrieveConfirmed: true with zero reports.The report pull had failed silently, and retrieved was counted after the usage fold drops those nodes, so the row could never be non-zero. The vault asserted a confirmed zero about an org with 4,296 reports. Failures are now recorded in the manifest and printed; a Report row can no longer claim a confirmed zero.
One validation rule reported as two separate blockers.A rule reaches its fields by two edges tokenized from one string, so it was printed under two categories with two counts. On the reference org that is 681 duplicated pairs, including 17 fields whose entire dependency set was one rule printed twice - the same inflated-referrer-count error this product's own field-audit method warns against. The presentation now folds onto one referrer and discloses the folded category; nothing is removed from the graph.
Every validation rule was labelled formula, with note text about "another formula field".The source marker is shared with the formula tokenizer and the rule keyed on it was unscoped, so the truthful validation category was unreachable in production. Now scoped to a CustomField referrer.
safe_to_delete_field advertised a traversalPath in its formula note.No type carried it, nothing populated it, no renderer emitted it. It is now surfaced per example, so a directly-tokenized formula reference is distinguishable from a resolved cross-object traversal.
Roll-up citations described the coupling.They described two of three roles - 32 of 98 roll-up edges are summaryFilterItem, where the roll-up's filter tests the field. Each example now carries its own rollupRole.
Condition citations named the firing rule.They named 4 of 7 wired firer families, so an ApprovalProcess blocker could be described as a Flow criterion. Every example now carries its firerId - the rule you actually go change.
FlexiPage related-list aliases were stamped declared.Both that and the formula-traversal branch are a regex scrape plus an inferred relationship join. No file declares that a page column is a given field. declared is the tier this product asks you to trust when a delete is on the line, so both branches are now parsed.
The published SBOM described what you install.It was generated against a package with no lockfile, so it emitted 19 direct entries and an empty dependency graph. It now resolves the transitive runtime closure from the workspace lockfile: 137 components, 138 graph nodes, with a component floor and a non-empty-graph check that fail the publish rather than shipping a hollow artifact.

Making the whole class a build failure

Fixing eight over-claims individually is worth little if the ninth ships next month. Two of the worst offenders were not analysis bugs at all - they were the project describing itself inaccurately. The official MCP Registry sat at 0.1.26 while npm served 0.2.5, and because downstream MCP directories crawl that registry record, one stale entry propagated the wrong version across the ecosystem. And llms.txt - the file published specifically so AI engines cite this project accurately - stated the Concept Model size twice, with two different figures, six lines apart.

0.3.0 closes the class structurally:

  • A generated ProductManifest. Tool counts, the core profile, live and local-mutation rosters, graph tables and schema version, Concept Model size and content hash, and a catalog hash are all derived from the runtime registries into eval/product-manifest.json, and exposed on sfi.capabilities. verify-doc-sync fails CI when the committed manifest, the website's data file, the README concept counts or the docs roster pins disagree with the registries. The 196-vs-209 and 94/143-vs-142/193 drift class cannot recur silently.
  • A tag can no longer publish unverified. The publish workflow fires on tag push; CI fires on push-to-main and pull request - never on tags. The two were fully decoupled, so tagging any commit shipped it to npm and the registry with no guarantee CI had ever run. (needs: cannot fix this; it only orders jobs within one workflow file.) The publish job now asks the API what CI actually did on the tagged SHA and fails closed on everything except a clean success - including the real hole, where CI never ran at all - and waits out an in-flight run rather than racing a fresh merge.
  • A pre-0.3.0 vault stops returning the false safe. Upgrading without re-refreshing leaves a graph missing this release's edges, so an older builder version now adds a trust limitation and routes an otherwise-safe verdict to review. It fails open on an unparseable version - a verdict is never downgraded on a guess.

Org metadata is data, never instructions

Field labels, descriptions and help text are strings someone in your org typed. Anything that reads them into an assistant's context is handling attacker-influenceable text. 0.3.0 makes that explicit rather than implicit: contracts expose UntrustedOrgText and a content policy, sfi.get_component and sfi.resolve carry additive labelOrgText / descriptionOrgText fields, and the MCP dispatcher stamps contentPolicy on every success envelope so a host treats org strings as data - never as instructions, and never as consent.

This is a labelling guarantee at the protocol boundary, not a claim that the host will honour it. For the full threat model - what a read-only server can and cannot promise about prompt injection - see AI safety and the security model.

Upgrade checklist

upgrading to 0.3.0
# 1. Take the new version (npx -y always resolves latest)
npx -y [email protected] mcp

# 2. Re-refresh — a pre-0.3.0 vault downgrades `safe` verdicts to `review`
sfi refresh --target-org <alias>

# 3. Re-grant live consent, if you use the live plane at all
#    v1 records are ignored; the new grant expires in 7 days
live_consent(grant: true)

# 4. Point any direct non-core calls at the gateway
run_analysis(name: "sfi.coverage_report", args: {})

# — or opt out of all of the above routing changes —
export SFI_TOOL_PROFILE=full

# 5. Verify what you installed came from the tagged commit
npm audit signatures

If you only read the vault offline and never touched the live plane, steps 2 and 4 are the whole upgrade. If you script against the server, step 4 is the one that will surface as a denied tools/call.

FAQ

What breaks when you upgrade sf-intelligence to 0.3.0?

Six things, all deliberate. The default tool profile is now core, so only 19 tools are directly invokable and a tools/call outside that set is denied. Per-call liveEnabled: true is no longer a consent path. Existing v1 consent records without a grantId are ignored. Grants now expire after 7 days and bind to a Salesforce OrgId and principal. The npm update check is opt-in rather than automatic. And every success envelope carries a contentPolicy stamp of roughly 280 bytes. Nothing else about tool inputs or outputs changed; legacy response keys are untouched.

Why does sf-intelligence advertise 19 tools instead of 209?

209 is the number of registered tools; 19 is the number advertised to the host by default. Publishing 205 schemas into an MCP host's tool list costs context on every single turn and makes the host's own routing worse, because most of those tools are specialist analyses that a general question should never be choosing between. The core profile is a 19-tool spine covering resolve and search, component and edge retrieval, impact, effective permissions, order of execution, org overview and history, health, routing and synthesis, tool discovery, and live consent - and everything else is reached through sfi.run_analysis by name. Set SFI_TOOL_PROFILE=full to restore the previous advertise-everything behaviour.

How do I call a non-core sf-intelligence tool after 0.3.0?

Call sfi.run_analysis with a name and an args object, for example { name: 'sfi.interpret', args: { componentId: 'CustomField:Account.Industry__c' } }. The target must be a registered tool, and the gateway validates it against that tool's real schema, so this is a routing change rather than an escape hatch. Use sfi.describe_analysis to get a tool's schema first; under the core profile it defaults to a summary detail level, with schema and full available on request.

Do you need to re-refresh the vault after upgrading to 0.3.0?

Yes, if you rely on delete-safety verdicts. A vault built by a pre-0.3.0 builder is missing edges that this release added, so a safe verdict from it is not trustworthy. Rather than silently serve the old answer, 0.3.0 detects the older builder version, attaches a trust limitation, and routes an otherwise-safe verdict to review - the same not-proven-safe treatment that incomplete coverage already receives. It fails open on an unparseable version, because a verdict should never be downgraded on a guess. Run sfi refresh against your org alias to clear it.

Why did live consent stop working after upgrading sf-intelligence?

Live consent became a v2 grant and v1 records are ignored by design, so you re-grant once with sfi.live_consent { grant: true }. The new grant binds to the Salesforce OrgId and the principal read from sf org display, carries scopes for aggregate, sample and users access, and expires after 7 days by default. Re-pointing an alias at a different org refuses the grant rather than silently answering about the new org. Sample and user-level tools additionally require an explicit scope step-up.

Related reading

  • The deterministic reasoning model - how sfi.interpret turns metadata into cited structural claims, and what 0.2.0 and 0.2.1 shipped.
  • Quality & trust - the full CI gate, the per-answer trust model, and the boundaries static analysis does not cross.
  • Configuration - SFI_TOOL_PROFILE, SFI_NETWORK_MODE, the live plane, and every other environment variable.
  • Capability map - the eight question areas behind the 19-tool core spine.

Run 0.3.0 against your own org.

Free, read-only, offline. Start on the synthetic demo org - no Salesforce auth, no sf CLI, nothing to configure - then point it at your own metadata.