cfs.manifests.* - manifest CRUD

Phase 10 transport note. Was /api/manifests/* in Phase 1–9. The same pure handlers in packages/core/src/handlers/manifests.ts now back the IPC channels below. Request/response shapes preserved - only the transport changed (HTTP → IPC). Renderer code calls window.cfs.manifests.* via apps/desktop/electron/preload.ts.

Authoring goes here. Deployment is separate (cfs.deploy.*). Registration is schema-only and never spawns the CLI - see Architecture → Registration semantics.

Complete IPC surface

This table is the current renderer preload surface from apps/desktop/electron/preload.ts. The macOS author build omits the flavor-conditional namespaces called out in apps/desktop/src/lib/flavor.ts. Legacy scenario and drift methods still exist only as compatibility stubs and always return a not-supported 501 envelope.

NamespaceMethod or eventIPC channelPurpose
cfs.activityrecent()cfs:activity:recentRead the recent activity feed.
cfs.auditPackget(req)cfs:audit-pack:getBuild an audit-pack artifact in memory.
cfs.auditPacksave(req)cfs:audit-pack:saveSave an audit-pack artifact through a native dialog.
cfs.auditResultsget(id)cfs:audit-results:getRead the last cached device-audit result.
cfs.baselineCsvfetch(req)cfs:baseline-csv:fetchFetch and parse a baseline CSV source.
cfs.cisstatus()cfs:cis:statusReport CIS catalog availability and diagnostics.
cfs.cislookup(req)cfs:cis:lookupLook up one resource against CIS data.
cfs.cisrevealDataDir()cfs:cis:reveal-data-dirOpen the runtime CIS data directory.
cfs.cisrecheck()cfs:cis:recheckClear CIS caches and re-read data.
cfs.ciswarmup()cfs:cis:warmupPre-parse discovered CIS catalogs.
cfs.cisbulkLookup(namespace, benchmarkFilename?)cfs:cis:bulk-lookupScore one manifest against a CIS benchmark.
cfs.compliancereport(req)cfs:compliance:reportBuild a CIS compliance report.
cfs.deployrun(req, onProgress?)cfs:deploy:run and cfs:deploy:progressRun deploy or audit with progress events.
cfs.deploycancel(jobId)cfs:deploy:cancelRequest cancellation for a running deploy job.
cfs.deployRecoverylistInterrupted()cfs:deploy:list-interruptedList orphaned deploy sentinels.
cfs.deployRecoverydismiss(namespace)cfs:deploy:dismiss-interruptedDismiss an interrupted-deploy sentinel.
cfs.diffmatrix(names)cfs:diff:matrixBuild the N-way manifest matrix.
cfs.diffmatrixXlsxSave(names)cfs:diff:matrix-xlsx:saveSave the matrix as an XLSX workbook.
cfs.docsget(name)cfs:docs:getRead generated docs for a manifest.
cfs.docsgenerate(req)cfs:docs:generateGenerate docs from manifest content.
cfs.driftlist()cfs:drift:listRetired compatibility stub; always returns 501 not supported.
cfs.exportChannelget(req)cfs:export:getReturn an export artifact.
cfs.exportChannelsave(req)cfs:export:saveSave an export artifact through a native dialog.
cfs.healthcheck()cfs:health:checkProbe OSConfig CLI health from cache.
cfs.healthrecheck()cfs:health:recheckClear health cache and reprobe.
cfs.historylist(req)cfs:history:listList snapshots or read one snapshot.
cfs.historysave(req)cfs:history:saveSave a manual history snapshot.
cfs.historydelete(req)cfs:history:deleteDelete a history snapshot.
cfs.importChannelopenAndParse()cfs:import:openAndParseOpen a file picker, read the file, and parse it.
cfs.importChannelfromContent(req)cfs:import:fromContentParse renderer-supplied text or bytes.
cfs.librarylist()cfs:library:listList bundled baseline library entries.
cfs.libraryget(req)cfs:library:getRead one library entry.
cfs.manifestslist(opts?)cfs:manifests:listList registered and optionally live manifests.
cfs.manifestsget(name, opts?)cfs:manifests:getRead one manifest summary.
cfs.manifestsgetSource(name)cfs:manifests:sourceRead registered source YAML.
cfs.manifestsfetchUri(uri)cfs:manifests:fetch-uriFetch remote manifest text without registration.
cfs.manifestsregister(req)cfs:manifests:registerRegister source YAML and metadata.
cfs.manifestsrestore(req)cfs:manifests:restoreRestore a just-deleted registration from recovery data.
cfs.manifestsdelete(name, options?)cfs:manifests:deleteDelete a registration and return recovery metadata.
cfs.manifestsstatus(name)cfs:manifests:statusProbe deployed state for one manifest.
cfs.platforminfo()cfs:platform:infoRead host platform and theme snapshot.
cfs.platformonThemeChanged(cb)cfs:platform:theme-changedSubscribe to OS theme changes.
cfs.rationalelist(id)cfs:rationale:listRead rationale entries for a manifest.
cfs.rationaleappend(req)cfs:rationale:appendAppend a rationale entry.
cfs.revertapply(req)cfs:revert:applyRevert a deployed namespace.
cfs.scenarioslist()cfs:scenarios:listRetired compatibility stub; always returns 501 not supported.
cfs.settingsget()cfs:settings:getRead user settings.
cfs.settingsset(patch)cfs:settings:setMerge and persist user settings.
cfs.shellopenExternal(url)cfs:shell:open-externalOpen an HTTP or HTTPS URL in the default browser.
cfs.systemisElevated()cfs:system:is-elevatedCheck current process elevation.
cfs.systemelevate()cfs:system:elevateRelaunch with OS elevation when supported.
cfs.systemConfigsummary()cfs:system-config:summarySummarize host system configuration.
cfs.systemConfigforManifest(name)cfs:system-config:getRead host configuration for one manifest.
cfs.updategetStatus()cfs:update:get-statusRead the current auto-update status.
cfs.updateonStatus(cb)cfs:update:statusSubscribe to auto-update status events.
cfs.updatecheck()cfs:update:checkManually check for updates.
cfs.updatedownload()cfs:update:downloadStart update download.
cfs.updatequitAndInstall()cfs:update:quit-and-installQuit and install a downloaded update.

cfs.manifests.list(opts?) - channel cfs:manifests:list

List all manifests. Disk-only on the hot path: enriches each registered namespace from ~/.configforge/manifests/<ns>.json without spawning the CLI. Pass { live: true } to also union in CLI-visible namespaces; those entries fall through to oscfg get namespace.

Two in-process caches (disk-only vs. live-union), 60s TTL, in-flight dedup via a generation token. Any register/delete invalidates both.

type ListManifestsOptions = {
  live?: boolean;
  includeResources?: boolean; // default true
  lite?: boolean;             // explicit opt-in to drop Resources[]
  force?: boolean;            // bypass list caches for explicit refresh
};

type ManifestSummary = {
  Name: string;
  DisplayName: string;
  Source: 'oscfg' | 'library';
  RegistrationSource: 'user' | 'library' | 'import' | null;
  RegistrationSourceId: string | null;
  Deployed: boolean;
  LastAppliedAt: string | null;
  LastAuditedAt: string | null;
  Revision: string | null;
  Platform: string | null;
  ResourceCount: number;
  Validation: {
    hasSchema: boolean;
    hasEnforcementValues: boolean;
    hasComplianceCriteria: boolean;
    issues: string[];
  } | null;
  Compliance: {
    auditedAt: string;
    total: number;
    compliant: number;
    nonCompliant: number;
    indeterminate: number;
    errors: number;
  } | null;
  RegisteredAt: string | null;
  LastModifiedAt: string | null;
  Resources?: { name: string; type: string }[]; // omitted when lite=true
};

const { data } = await cfs.manifests.list();
// or
const { data } = await cfs.manifests.list({ live: true, lite: true });

Field casing is intentionally PascalCase for back-compat with the PowerShell-edition UI. Source is "oscfg" (locally-authored) or "library" (catalog-sourced). Resources is [] for CLI-only namespaces; the detail page fetches them via cfs.manifests.get(name).

cfs.manifests.get(name, opts?) - channel cfs:manifests:get

Single-manifest read (perf W2 / C5). Returns { data: null } (does not throw) for unknown namespaces.

const { data, warning } = await cfs.manifests.get('ws2025-baseline');
// data: ManifestSummary | null
// warning?: string - surfaces e.g. "Registered source YAML missing on disk"

cfs.manifests.getSource(name) - channel cfs:manifests:source

Return the registered source YAML exactly as stored on disk, or { data: null } when the namespace is not registered. This is distinct from status, which reads/reconstructs live state from oscfg.

const { data } = await cfs.manifests.getSource('ws2025-baseline');
// data: string | null

cfs.manifests.fetchUri(uri) - channel cfs:manifests:fetch-uri

Fetch remote YAML for preview/edit without registering it. The same URL validation and 10 MB / 30 s fetch limits used by register({ uri }) apply; the response is { content: string }.

cfs.manifests.restore(req) - channel cfs:manifests:restore

Restore a just-deleted registration from captured recovery data without overwriting an existing namespace. The restore path validates the YAML content and writes only the registration metadata and source YAML; it does not reconstruct deployment pointers, history, rationale, or audit records.

type RestoreManifestRequest = {
  namespace: string;
  displayName: string;
  content: string;
  source: 'user' | 'library' | 'import';
  sourceId?: string;
};

type RestoreManifestResult = {
  message: string;
  data: {
    namespace: string;
    platform: 'windows' | 'linux' | 'mixed' | 'cross-platform' | 'unknown';
  };
};

Errors: 400 for invalid request, content, or schema; 409 when the namespace is already registered.

cfs.manifests.register(req) - channel cfs:manifests:register

Register a new manifest. Schema validation only - never calls oscfg apply. Best-effort auto-snapshot to ~/.configforge/history/<ns>/ runs in the background (failures logged, not surfaced).

type RegisterManifestRequest = {
  name: string;                              // display name - sanitized to namespace
  content?: string;                          // YAML or JSON; content or uri is required. Non-empty content wins over uri; an empty content string falls back to uri.
  uri?: string;                              // http/https URL (10 MB cap, 30 s timeout)
  // path is a rejected legacy field; use importChannel or content instead.
  source?: 'user' | 'library' | 'import';    // default 'user'
  sourceId?: string;
  rationale?: string;                        // persisted on snapshot .meta sidecar
  changeSummary?: string;                    // v0.3.47; max 200 chars at IPC boundary
  force?: boolean;                           // override namespace-collision guard
  author?: string;                           // test affordance - server resolves via resolveAuthor()
};

type RegisterManifestResult = {
  message: string;
  data: { namespace: string; platform: 'windows' | 'linux' | 'mixed' | 'cross-platform' | 'unknown' };
  warnings: string[];
};

changeSummary is the short diff-derived label shown in History instead of the generic "Manifest registered" message. The renderer computes it from the save diff; IPC validation caps it at 200 characters.

Hard 400 via HandlerError / IPC validation:

  • Missing name, or sanitizeNamespace(name) empty.
  • Both content and uri absent or empty (whitespace-only counts as empty).
  • Legacy path is rejected by validateRegisterManifestRequest() and by the core handler; use importChannel or pass already-read YAML via content.
  • Remote uri > 10 MB → HandlerError(413).
  • Unsupported URI scheme (only http: / https:).
  • Content fails YAML/JSON parse or validateManifestSchema.

Soft warnings (registration succeeds, populated in warnings[]):

  • Manifest targets a different platform than the host.
  • Mixed Windows + Linux resource types.
  • Resource types not in the host-platform registered-type whitelist.
  • Namespace collision: a different display name already maps to the same sanitized namespace (#20). The renderer can pass force: true to override.

Cross-platform authoring is supported. Deploy/audit are the gates that enforce platform match.

On macOS, platform-mismatch warnings are suppressed in the UI because the macOS author build has no deploy capability.

cfs.manifests.delete(name) - channel cfs:manifests:delete

Remove a registration. Best-effort CLI cleanup (oscfg delete namespace) runs in parallel; failures don't block. Also cleans the per-manifest rationale log and the cached audit-results JSON (v0.1.6).

type DeleteManifestResult = {
  message: string;
  data: {
    namespace: string;
    cliRemoved: boolean;        // false for registered-but-never-deployed - expected, not a failure
    cliError: string | null;
    rationaleLogRemoved: boolean;
    rationaleLogError: string | null;
    recovery: RegistrationRecoveryBackup | null;
  };
};

cfs.manifests.status(name) - channel cfs:manifests:status

Per-manifest deployed-state probe (read-only). 5s cache + in-flight dedup. Returns a soft stub when the manifest is registered but never deployed, instead of an error.

// Deployed namespace
{
  data: '# Reported system configuration for: ws2025-baseline\n...',
  name: 'ws2025-baseline',
  resources: [/* live resources from oscfg get resource */],
  deployed: true,
}

// Registered but never deployed (soft stub, not an error)
{
  data: '# ws2025-baseline is registered but not yet deployed on this host.\n...',
  name: 'ws2025-baseline',
  resources: [],
  deployed: false,
  cliError: 'Namespace not found',
}

Error shape

Handlers throw HandlerError(status, message) (see packages/core/src/handlers/errors.ts). The IPC layer wraps to { ok: false, status, error, code? }; the preload call<T>() helper re-throws as a regular Error with .status (and .code) attached.

See also