tryApplyStagedKbcPatch method
Look for a KBC patch that a previous session staged on disk and apply it now, before runApp. Returns the apply result, or null if no patch was staged (or the staged patch is invalid).
This is the boot-time apply that closes the OTA loop end-to-end:
fetchAndApplyKbcPatch persists the envelope at
<App Documents>/sankofa-deploy/patches/active/patch.skdp, but
the patch's effect (e.g. JSON UI overrides decoded by the host)
lives only in memory for that session. On the next cold boot the
host must re-apply from disk — otherwise the user sees the
baseline UI again and "OTA" feels broken. Call this from main()
after Sankofa.init(...) and BEFORE runApp(...):
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Sankofa.instance.init(enableDeploy: true);
final stagedResult = await Sankofa.instance.deploy?.tryApplyStagedKbcPatch(
loader: loadModuleFromBytes,
);
runApp(MyApp(initialPatch: stagedResult));
}
Signature verification + ζ.1 engine check + sha-256 integrity all
run on the staged file just like a fresh fetch — bytes from disk
are treated with the same fail-closed posture as bytes from B2.
A signature mismatch (e.g. customer rotated keys via
sankofa keys generate AFTER staging this patch) returns null,
not an exception, so the app boots clean to baseline.
Throws StateError if Sankofa.deploy wasn't enabled — calling
this on a deploy-disabled app is almost certainly a bug. Other
failures (no file, parse error, sig mismatch, loader error) all
return null so the cold-start path stays defensive.
Implementation
Future<KbcPatchResult?> tryApplyStagedKbcPatch({
required KbcLoaderFn loader,
}) async {
final patchPath = await _kbcSlotPath(_kbcSlotActive);
if (!File(patchPath).existsSync()) return null;
// STALE-BASE GATE. A staged envelope outlives the binary it was built
// for: the app's data container survives an App Store update, so a patch
// staged against version X is still sitting here when the user launches
// version Y. Its bytecode was compiled against X's base kernel, so
// transplanting it into Y is undefined — in practice a boot crash. The
// rollback gate below WOULD eventually catch that, but only after
// crashing the user repeatedly. A patch that cannot possibly belong to
// this binary must never be applied even once: drop it and let the
// startup fetch pull the one built for this version.
if (!await _discardStagedPatchIfWrongBinary(patchPath)) return null;
// Rollback gate (time-window model — RN parity). Counts a boot as
// a "crash" only if the previous boot didn't confirm healthy AND
// was within kKbcCrashWindow. Two such consecutive crashes trip
// the rollback.
final prefs = await SharedPreferences.getInstance();
final nowMs = DateTime.now().millisecondsSinceEpoch;
// Migrate from the legacy boot-count model: if the old counter
// exists, ignore it (no decision based on it) and just delete.
if (prefs.containsKey(_kbcBootCounterKey)) {
await prefs.remove(_kbcBootCounterKey);
}
final lastBootMs = prefs.getInt(_kbcLastBootTimeMsKey);
final lastBootConfirmed = prefs.getBool(_kbcBootConfirmedKey) ?? false;
int crashCount = prefs.getInt(_kbcCrashCountKey) ?? 0;
final previousBootCrashed = !lastBootConfirmed &&
lastBootMs != null &&
(nowMs - lastBootMs) < kKbcCrashWindow.inMilliseconds;
if (previousBootCrashed) {
crashCount += 1;
await prefs.setInt(_kbcCrashCountKey, crashCount);
} else if (lastBootConfirmed) {
// Clean reset on any healthy prior boot.
crashCount = 0;
await prefs.setInt(_kbcCrashCountKey, 0);
}
if (crashCount >= kKbcCrashThreshold) {
// Best-effort: parse the bad envelope's label before moving it
// aside, so we can record it in the banned set + tell the
// dashboard which release failed. Parse-only — no apply.
String? rolledBackLabel;
try {
final bytes = await File(patchPath).readAsBytes();
final parsed = parseKbcEnvelope(bytes);
final lbl = parsed.metadata['label'];
if (lbl is String && lbl.isNotEmpty) {
rolledBackLabel = lbl;
await prefs.setString(_kbcLastDisabledKey, lbl);
// Ban the label so /api/deploy/check serving the same patch
// again doesn't crash-loop: fetchAndApplyKbcPatch consults
// _kbcBannedLabelsKey before applying.
await _addBannedLabel(prefs, lbl);
}
} catch (_) {
// Parse failed too — wedged envelope; can't ban the label but
// the file still gets moved aside.
}
try {
final disabledDir = await _kbcSlotDir(_kbcSlotDisabled);
if (!disabledDir.existsSync()) {
disabledDir.createSync(recursive: true);
}
final disabledPath =
'${disabledDir.path}/patch.skdp.disabled-${DateTime.now().millisecondsSinceEpoch}';
await File(patchPath).rename(disabledPath);
if (kDebugMode) {
debugPrint(
'[Sankofa.deploy] AUTO-ROLLBACK: staged patch "$rolledBackLabel" '
'crashed $crashCount consecutive boots inside ${kKbcCrashWindow.inSeconds}s '
'— moved to $disabledPath.',
);
}
} catch (_) {
// Rename failed — disk full or permission. Still bail out of
// applying; don't keep crash-looping.
}
// Reset chain-safety state — we just rolled back.
await prefs.remove(_kbcCrashCountKey);
await prefs.setBool(_kbcBootConfirmedKey, false);
await prefs.setInt(_kbcLastBootTimeMsKey, nowMs);
// Restore last_good → active if we have one. Customer doesn't
// drop all the way back to baseline — falls back to the most
// recent patch that actually survived a boot.
final restoredFromLastGood = await _restoreLastGoodToActive();
if (restoredFromLastGood) {
if (kDebugMode) {
debugPrint(
'[Sankofa.deploy] AUTO-ROLLBACK: restored last_good patch to active. '
'Next boot will apply the previous known-good patch.',
);
}
unawaited(_reportKbcEvent(
eventType: 'kbc_patch_restored_from_last_good',
bundleLabel: rolledBackLabel,
extra: {'reason': 'rollback_threshold_exceeded'},
));
}
unawaited(_reportKbcEvent(
eventType: 'kbc_boot_apply_skipped_rollback',
bundleLabel: rolledBackLabel,
extra: {'restored_last_good': restoredFromLastGood},
));
return null;
}
// Record this boot. boot_confirmed flips to true only when the host
// calls notifyKbcPatchReady() or the 10s auto-confirm timer fires.
await prefs.setInt(_kbcLastBootTimeMsKey, nowMs);
await prefs.setBool(_kbcBootConfirmedKey, false);
try {
final result = await kbc_loader.applyKbcEnvelopeFromFile(
patchPath,
loader: loader,
signingPubkeysB64: _effectiveSigningPubkeys,
);
unawaited(_reportKbcEvent(
eventType: 'kbc_boot_apply_success',
bundleLabel: result.metadata['label']?.toString(),
));
// Arm the 10s auto-confirm safety net (RN parity). If the host
// never calls notifyKbcPatchReady, we'll auto-ack so a clean
// 10-second-old app session is treated as proof the patch is
// healthy. notifyKbcPatchReady cancels this and confirms exactly.
_armAutoConfirmTimer();
return result;
} on KbcApplyException catch (err) {
if (kDebugMode) {
debugPrint(
'[Sankofa.deploy] staged patch at $patchPath failed to apply: ${err.message}'
'${err.cause != null ? '\n[Sankofa.deploy] cause: ${err.cause}' : ''}'
'${err.causeStackTrace != null ? '\n[Sankofa.deploy] cause stack:\n${err.causeStackTrace}' : ''}',
);
}
final stack = _formatKbcStackTrace(err.causeStackTrace);
unawaited(_reportKbcEvent(
eventType: 'kbc_boot_apply_failed',
errorMessage: err.message,
extra: {
'phase': 'boot_apply',
'cause_class': 'KbcApplyException',
if (err.cause != null) 'inner_cause': err.cause.toString(),
if (err.cause != null)
'inner_cause_class': err.cause.runtimeType.toString(),
if (stack != null) 'stack_trace': stack,
},
));
return null;
} catch (err, st) {
if (kDebugMode) {
debugPrint(
'[Sankofa.deploy] unexpected error applying staged patch at $patchPath: $err',
);
}
final stack = _formatKbcStackTrace(st);
unawaited(_reportKbcEvent(
eventType: 'kbc_boot_apply_failed',
errorMessage: err.toString(),
extra: {
'phase': 'boot_apply',
'cause_class': err.runtimeType.toString(),
if (stack != null) 'stack_trace': stack,
},
));
return null;
}
}