local-operator-ui 0.29.6 → 0.29.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/main/index.js +460 -28
- package/out/renderer/assets/{_basePickBy-D-VwFtKc.js → _basePickBy-ymj-o8fv.js} +1 -1
- package/out/renderer/assets/{_baseUniq-DDJz-WZw.js → _baseUniq-C7octmF9.js} +1 -1
- package/out/renderer/assets/{agent-details-page-jO2PvB7X.js → agent-details-page-CKDC6wi8.js} +1 -1
- package/out/renderer/assets/{agent-hub-page-xdes-D_X.js → agent-hub-page-COrdCxjK.js} +1 -1
- package/out/renderer/assets/{agents-page-j8DAg52R.js → agents-page-DS_tctSR.js} +2 -2
- package/out/renderer/assets/{arc-CjaBvXDL.js → arc-Clir9ORn.js} +1 -1
- package/out/renderer/assets/{architectureDiagram-IEHRJDOE-CYVtl1Tr.js → architectureDiagram-IEHRJDOE-xcqIlSLn.js} +1 -1
- package/out/renderer/assets/{blockDiagram-JOT3LUYC-DfMbDEPW.js → blockDiagram-JOT3LUYC-CCnhXTN9.js} +1 -1
- package/out/renderer/assets/browser-page-CZ1C-JPi.js +1 -0
- package/out/renderer/assets/{browser-webauthn-prompt-3S4O_0eV.js → browser-webauthn-prompt-RatX-ifB.js} +1 -1
- package/out/renderer/assets/{c4Diagram-VJAJSXHY-D_lTZwGe.js → c4Diagram-VJAJSXHY-ZTdwqac9.js} +1 -1
- package/out/renderer/assets/channel-ClDezImr.js +1 -0
- package/out/renderer/assets/{chunk-4BMEZGHF-BPOpt_G1.js → chunk-4BMEZGHF-cttO-nIF.js} +1 -1
- package/out/renderer/assets/{chunk-A2AXSNBT-DTq_POy_.js → chunk-A2AXSNBT-BhbCOJ0v.js} +1 -1
- package/out/renderer/assets/{chunk-AEK57VVT-DOAjbTBV.js → chunk-AEK57VVT-Br7bGky6.js} +1 -1
- package/out/renderer/assets/{chunk-D6G4REZN-CLeUkHhx.js → chunk-D6G4REZN-Cm0LM5Ud.js} +1 -1
- package/out/renderer/assets/{chunk-RZ5BOZE2-DR3q6hDQ.js → chunk-RZ5BOZE2-CgDNm20q.js} +1 -1
- package/out/renderer/assets/{chunk-XZIHB7SX-DeI7Tei8.js → chunk-XZIHB7SX-BDuUKAGE.js} +1 -1
- package/out/renderer/assets/classDiagram-GIVACNV2-DXnKRSrI.js +1 -0
- package/out/renderer/assets/classDiagram-v2-COTLJTTW-DXnKRSrI.js +1 -0
- package/out/renderer/assets/clone-BSHfbpRK.js +1 -0
- package/out/renderer/assets/{compact-pagination-pR_TewmZ.js → compact-pagination-Cic_HwuH.js} +1 -1
- package/out/renderer/assets/{dagre-OKDRZEBW-BzCAmNFs.js → dagre-OKDRZEBW-qWFY5zf_.js} +1 -1
- package/out/renderer/assets/{diagram-SSKATNLV-DJhkbiTK.js → diagram-SSKATNLV-B8Ih5A2a.js} +1 -1
- package/out/renderer/assets/{diagram-VNBRO52H-C198p3CS.js → diagram-VNBRO52H-C596SkIn.js} +1 -1
- package/out/renderer/assets/{erDiagram-Q7BY3M3F-BXMHZzg7.js → erDiagram-Q7BY3M3F-CvnaIAqr.js} +1 -1
- package/out/renderer/assets/{flowDiagram-4HSFHLVR-BH25N-UK.js → flowDiagram-4HSFHLVR-BFPwO2a8.js} +1 -1
- package/out/renderer/assets/{ganttDiagram-APWFNJXF-C90nUk9-.js → ganttDiagram-APWFNJXF-DJtCXZy3.js} +1 -1
- package/out/renderer/assets/{gitGraphDiagram-7IBYFJ6S-CGCN32Lb.js → gitGraphDiagram-7IBYFJ6S-CawG1eU0.js} +1 -1
- package/out/renderer/assets/{graph-CnshGmMM.js → graph-CMxpK-8p.js} +1 -1
- package/out/renderer/assets/icon-BFIQ-3RP.css +1 -0
- package/out/renderer/assets/{index-Dic-nJw9.js → index-2zEOm1qn.js} +1 -1
- package/out/renderer/assets/{index-CLOkmTr3.js → index-BYBfIqlw.js} +1 -1
- package/out/renderer/assets/{index-CbEqOOj5.js → index-ZqkmhE-p.js} +383 -378
- package/out/renderer/assets/{infoDiagram-PH2N3AL5-yhFK8QbZ.js → infoDiagram-PH2N3AL5-D_9mC1qn.js} +1 -1
- package/out/renderer/assets/{installer-DL2KRfdq.js → installer-BISA9sy1.js} +1 -1
- package/out/renderer/assets/{journeyDiagram-U35MCT3I-Bh_mQdVI.js → journeyDiagram-U35MCT3I-DD6wEbWI.js} +1 -1
- package/out/renderer/assets/{kanban-definition-NDS4AKOZ-CFxjCPF4.js → kanban-definition-NDS4AKOZ-4g9wB8rI.js} +1 -1
- package/out/renderer/assets/{layout-CEBnys_f.js → layout-C3S9kkxy.js} +1 -1
- package/out/renderer/assets/{legacy-agents-page-BV5wNeFT.js → legacy-agents-page-CCPzPI8i.js} +1 -1
- package/out/renderer/assets/{mermaid.core-CItomUrR.js → mermaid.core-p4H_NhJR.js} +5 -5
- package/out/renderer/assets/{mindmap-definition-ALO5MXBD-B-uWszwf.js → mindmap-definition-ALO5MXBD-qeMXC1FV.js} +1 -1
- package/out/renderer/assets/{page-header-D1a45b0y.js → page-header-D2DgPsOk.js} +1 -1
- package/out/renderer/assets/{parseISO-Dkhkl_3Q.js → parseISO-BX7eTZ2s.js} +1 -1
- package/out/renderer/assets/{pieDiagram-IB7DONF6-CqJrFz8P.js → pieDiagram-IB7DONF6-CzgApFtA.js} +1 -1
- package/out/renderer/assets/{quadrantDiagram-7GDLP6J5-Bla_rG-3.js → quadrantDiagram-7GDLP6J5-D4CGTVNr.js} +1 -1
- package/out/renderer/assets/{radar-MK3ICKWK-DM-Fttuo.js → radar-MK3ICKWK-ADVDebjf.js} +1 -1
- package/out/renderer/assets/{radient-auth-buttons-5BZmMSkU.js → radient-auth-buttons-Bj6LnVZy.js} +1 -1
- package/out/renderer/assets/{requirementDiagram-KVF5MWMF-f1n0Vj3U.js → requirementDiagram-KVF5MWMF-ByMCblO3.js} +1 -1
- package/out/renderer/assets/{sankeyDiagram-QLVOVGJD-WBvrc_GC.js → sankeyDiagram-QLVOVGJD-jZZcCcqS.js} +1 -1
- package/out/renderer/assets/{schedules-page-CufYrWWt.js → schedules-page-CcXnTDkD.js} +1 -1
- package/out/renderer/assets/{sequenceDiagram-X6HHIX6F-DXNQl5UM.js → sequenceDiagram-X6HHIX6F-D4Zquhvo.js} +1 -1
- package/out/renderer/assets/settings-page-Cas89rlW.js +317 -0
- package/out/renderer/assets/{stateDiagram-DGXRK772-Dc-HE3Jh.js → stateDiagram-DGXRK772-64taMWN_.js} +1 -1
- package/out/renderer/assets/stateDiagram-v2-YXO3MK2T-_UADfAkc.js +1 -0
- package/out/renderer/assets/{timeline-definition-BDJGKUSR-DAE8SRiO.js → timeline-definition-BDJGKUSR-Boc0hio0.js} +1 -1
- package/out/renderer/assets/{use-agent-like-mutation-BmPMhO1j.js → use-agent-like-mutation-awmzBb6p.js} +1 -1
- package/out/renderer/assets/{xychartDiagram-VJFVF3MP-DoRBXwwS.js → xychartDiagram-VJFVF3MP-DY0skouT.js} +1 -1
- package/out/renderer/index.html +3 -3
- package/out/renderer/installer.html +3 -3
- package/package.json +2 -2
- package/out/renderer/assets/browser-page-Dw1FPkTB.js +0 -1
- package/out/renderer/assets/channel-Dfi7uOIV.js +0 -1
- package/out/renderer/assets/classDiagram-GIVACNV2-Dln1fC0q.js +0 -1
- package/out/renderer/assets/classDiagram-v2-COTLJTTW-Dln1fC0q.js +0 -1
- package/out/renderer/assets/clone-DA5JBsPm.js +0 -1
- package/out/renderer/assets/icon-DryJ1IIw.css +0 -1
- package/out/renderer/assets/settings-page-BppBUxvd.js +0 -317
- package/out/renderer/assets/stateDiagram-v2-YXO3MK2T-CEPLahMP.js +0 -1
- /package/out/renderer/assets/{icon-B19NuJYm.js → icon-Dr9ZV2SX.js} +0 -0
package/out/main/index.js
CHANGED
|
@@ -38,8 +38,16 @@ const path__namespace = /* @__PURE__ */ _interopNamespaceDefault(path$1);
|
|
|
38
38
|
const https__namespace = /* @__PURE__ */ _interopNamespaceDefault(https);
|
|
39
39
|
const icon = path.join(__dirname, "../../resources/icon.png");
|
|
40
40
|
const BACKEND_STATUS_CHANNEL = "backend-status";
|
|
41
|
+
const DAEMON_PAIRED = { available: true, cause: null };
|
|
42
|
+
const DAEMON_UNPAIRED = {
|
|
43
|
+
available: false,
|
|
44
|
+
cause: "unpaired"
|
|
45
|
+
};
|
|
41
46
|
const BACKEND_STATUS_EVENT = "backend-status-changed";
|
|
42
47
|
const BACKEND_RECONNECT_CHANNEL = "backend-reconnect";
|
|
48
|
+
function relayNeedsRebuild(current, next) {
|
|
49
|
+
return current.url !== next.url || current.token !== next.token;
|
|
50
|
+
}
|
|
43
51
|
const id$1 = zod.z.string().min(1).max(128).regex(/^[a-zA-Z0-9_-]+$/);
|
|
44
52
|
const settingKey = zod.z.string().min(1).max(256).regex(/^[a-zA-Z0-9_][a-zA-Z0-9_.-]*$/);
|
|
45
53
|
const secret = zod.z.string().min(1).max(32768);
|
|
@@ -1024,6 +1032,51 @@ const desktopRequestSchema = zod.z.discriminatedUnion("op", [
|
|
|
1024
1032
|
value: secret
|
|
1025
1033
|
}).strict()
|
|
1026
1034
|
]);
|
|
1035
|
+
const DESKTOP_REFUSAL_CODE = {
|
|
1036
|
+
/**
|
|
1037
|
+
* This app holds no token for the daemon it can see, so main refused to send
|
|
1038
|
+
* the request at all. A PAIRING condition: the daemon never answered.
|
|
1039
|
+
*/
|
|
1040
|
+
noCredential: "pairing.no-credential",
|
|
1041
|
+
/**
|
|
1042
|
+
* A 401/403 from a `/v1/desktop/` route: the daemon answered and refused this
|
|
1043
|
+
* app's bearer. Re-claiming is the repair that exists, so it is a pairing
|
|
1044
|
+
* condition and not a version one.
|
|
1045
|
+
*/
|
|
1046
|
+
refused: "pairing.refused",
|
|
1047
|
+
/**
|
|
1048
|
+
* A 503 from a `/v1/desktop/` route that the app did NOT author: the daemon is
|
|
1049
|
+
* running with its desktop plane shut (no claim accepted, no environment
|
|
1050
|
+
* token), which is what a successor answers with until the app claims it.
|
|
1051
|
+
*/
|
|
1052
|
+
planeClosed: "pairing.plane-closed",
|
|
1053
|
+
/**
|
|
1054
|
+
* Main could not complete the request at all (a refused socket, a reset). NOT a
|
|
1055
|
+
* pairing condition - nothing was established about the daemon's plane - and it
|
|
1056
|
+
* carries its own authored sentence.
|
|
1057
|
+
*/
|
|
1058
|
+
transportFailed: "transport.failed"
|
|
1059
|
+
};
|
|
1060
|
+
const DESKTOP_MACHINE_DETAIL = {
|
|
1061
|
+
/*
|
|
1062
|
+
* NOTE for whoever greps for the ownership instruction this change removed:
|
|
1063
|
+
* `noCredential` still READS like that sentence, and deliberately. The code is
|
|
1064
|
+
* what `userFacingMessage` keys on, so this string is never rendered; it exists
|
|
1065
|
+
* because the development proxy forwards a refusal body without declaring one,
|
|
1066
|
+
* and `mcp-failure.ts` has to recognise main's own 503 by its text there. It is
|
|
1067
|
+
* a machine vocabulary, not copy - which is why the sentence a user reads comes
|
|
1068
|
+
* from `userFacingMessage` and this one is only ever compared (review round 1,
|
|
1069
|
+
* NIT-5).
|
|
1070
|
+
*/
|
|
1071
|
+
noCredential: "Restart with a desktop-managed backend to use these controls.",
|
|
1072
|
+
transportFailed: "The backend could not complete this request. Check its connection and try again."
|
|
1073
|
+
};
|
|
1074
|
+
function desktopRefusalCodeForStatus(path2, status2) {
|
|
1075
|
+
if (!path2.startsWith("/v1/desktop/")) return void 0;
|
|
1076
|
+
if (status2 === 401 || status2 === 403) return DESKTOP_REFUSAL_CODE.refused;
|
|
1077
|
+
if (status2 === 503) return DESKTOP_REFUSAL_CODE.planeClosed;
|
|
1078
|
+
return void 0;
|
|
1079
|
+
}
|
|
1027
1080
|
const DESKTOP_MESSAGE_BYTE_BUDGET = 88e4;
|
|
1028
1081
|
const DESKTOP_CONTROL_BYTE_BUDGET = 262144;
|
|
1029
1082
|
const DESKTOP_SYSTEM_PROMPT_BYTE_BUDGET = 11e5;
|
|
@@ -2335,6 +2388,8 @@ async function claimDesktopPlane(address, claimKey, options = {}) {
|
|
|
2335
2388
|
if (response.status === 409)
|
|
2336
2389
|
return { outcome: "already-claimed", status: 409 };
|
|
2337
2390
|
if (response.status === 401) return { outcome: "wrong-key", status: 401 };
|
|
2391
|
+
if (response.status === 404)
|
|
2392
|
+
return { outcome: "no-handshake", status: 404 };
|
|
2338
2393
|
return {
|
|
2339
2394
|
outcome: "refused",
|
|
2340
2395
|
status: response.status,
|
|
@@ -2387,6 +2442,14 @@ const BUNDLED_PYTHON_LAYOUT = {
|
|
|
2387
2442
|
checkoutSeedNames,
|
|
2388
2443
|
machOMagics
|
|
2389
2444
|
};
|
|
2445
|
+
const embeddedProvisioningProfilePath = "Contents/embedded.provisionprofile";
|
|
2446
|
+
const unrestrictedEntitlementKeys = ["com.apple.security.app-sandbox", "com.apple.security.application-groups", "com.apple.security.get-task-allow", "com.apple.security.inherit", "com.apple.security.print", "com.apple.security.smartcard"];
|
|
2447
|
+
const unrestrictedEntitlementPrefixes = ["com.apple.security.cs.", "com.apple.security.network.", "com.apple.security.files.", "com.apple.security.device.", "com.apple.security.personal-information.", "com.apple.security.assets.", "com.apple.security.automation."];
|
|
2448
|
+
const MACOS_ENTITLEMENT_POLICY = {
|
|
2449
|
+
embeddedProvisioningProfilePath,
|
|
2450
|
+
unrestrictedEntitlementKeys,
|
|
2451
|
+
unrestrictedEntitlementPrefixes
|
|
2452
|
+
};
|
|
2390
2453
|
const PENDING_INSTALL_MARKER_FILE = "pending-update-install.json";
|
|
2391
2454
|
const INSTALL_DISK_SLACK_BYTES = 256 * 1024 * 1024;
|
|
2392
2455
|
const DOWNLOAD_PAGE_URL = "https://local-operator.com/download";
|
|
@@ -2688,6 +2751,70 @@ function resolveStagedArtifactPath(input) {
|
|
|
2688
2751
|
}
|
|
2689
2752
|
return null;
|
|
2690
2753
|
}
|
|
2754
|
+
const EMBEDDED_PROVISIONING_PROFILE_PATH = MACOS_ENTITLEMENT_POLICY.embeddedProvisioningProfilePath;
|
|
2755
|
+
function isProfileBackedEntitlement(key) {
|
|
2756
|
+
if (MACOS_ENTITLEMENT_POLICY.unrestrictedEntitlementKeys.includes(key)) {
|
|
2757
|
+
return false;
|
|
2758
|
+
}
|
|
2759
|
+
return !MACOS_ENTITLEMENT_POLICY.unrestrictedEntitlementPrefixes.some(
|
|
2760
|
+
(prefix) => key.startsWith(prefix)
|
|
2761
|
+
);
|
|
2762
|
+
}
|
|
2763
|
+
function profileBackedEntitlementKeys(plistText) {
|
|
2764
|
+
const keys = [];
|
|
2765
|
+
for (const match of String(plistText ?? "").matchAll(
|
|
2766
|
+
/<key>([^<]+)<\/key>/g
|
|
2767
|
+
)) {
|
|
2768
|
+
const key = match[1];
|
|
2769
|
+
if (isProfileBackedEntitlement(key) && !keys.includes(key)) keys.push(key);
|
|
2770
|
+
}
|
|
2771
|
+
return keys;
|
|
2772
|
+
}
|
|
2773
|
+
function zipListingHasEmbeddedProfile(listing) {
|
|
2774
|
+
const suffix = `/${EMBEDDED_PROVISIONING_PROFILE_PATH}`;
|
|
2775
|
+
return String(listing ?? "").split("\n").some((entry) => entry.trim().endsWith(suffix));
|
|
2776
|
+
}
|
|
2777
|
+
function zipMainExecutableEntry(listing) {
|
|
2778
|
+
for (const raw of String(listing ?? "").split("\n")) {
|
|
2779
|
+
const entry = raw.trim();
|
|
2780
|
+
if (!entry || entry.endsWith("/")) continue;
|
|
2781
|
+
if (/^[^/]+\.app\/Contents\/MacOS\/[^/]+$/.test(entry)) return entry;
|
|
2782
|
+
}
|
|
2783
|
+
return null;
|
|
2784
|
+
}
|
|
2785
|
+
function stagedSignatureBlock(facts) {
|
|
2786
|
+
if (facts.entitlementsPlist == null) {
|
|
2787
|
+
if (facts.embeddedProfile) return null;
|
|
2788
|
+
return {
|
|
2789
|
+
code: "artifact-cannot-launch",
|
|
2790
|
+
heading: "The update couldn't be checked",
|
|
2791
|
+
message: facts.version ? `The update to version ${facts.version} can't be checked for launch, so it wasn't installed.` : "The downloaded update can't be checked for launch, so it wasn't installed.",
|
|
2792
|
+
remedy: {
|
|
2793
|
+
text: "Check for updates again to re-download the release."
|
|
2794
|
+
},
|
|
2795
|
+
detail: `${facts.artifactName} carries no ${EMBEDDED_PROVISIONING_PROFILE_PATH} and its signature could not be read.`,
|
|
2796
|
+
dismissLabel: "Not now"
|
|
2797
|
+
};
|
|
2798
|
+
}
|
|
2799
|
+
const claimed = profileBackedEntitlementKeys(facts.entitlementsPlist);
|
|
2800
|
+
if (claimed.length === 0) return null;
|
|
2801
|
+
if (facts.embeddedProfile) return null;
|
|
2802
|
+
return {
|
|
2803
|
+
code: "artifact-cannot-launch",
|
|
2804
|
+
message: facts.version ? `The update to version ${facts.version} can't be launched by macOS, so it wasn't installed.` : "The downloaded update can't be launched by macOS, so it wasn't installed.",
|
|
2805
|
+
remedy: {
|
|
2806
|
+
text: facts.version ? `Keep using this copy, and skip version ${facts.version} if you download one by hand — the next release will be offered here as usual.` : "Keep using this copy; the next release will be offered here as usual."
|
|
2807
|
+
},
|
|
2808
|
+
// The facts a reader can check, and nothing else: which archive, which
|
|
2809
|
+
// claim, which file macOS found missing. TN3125's rule (a restricted
|
|
2810
|
+
// entitlement needs a profile) is what makes the refusal TRUE, but a
|
|
2811
|
+
// reader who wants the rule wants Apple's document, and 250 characters of
|
|
2812
|
+
// it in a mono column made the details block 44% of the panel (design
|
|
2813
|
+
// round 1, D4). It belongs in this comment, not in the card.
|
|
2814
|
+
detail: `${facts.artifactName} claims ${claimed.join(", ")} and carries no ${EMBEDDED_PROVISIONING_PROFILE_PATH}.`,
|
|
2815
|
+
dismissLabel: "Not now"
|
|
2816
|
+
};
|
|
2817
|
+
}
|
|
2691
2818
|
function pendingInstallMarkerPath(dir) {
|
|
2692
2819
|
return path$1.join(dir, PENDING_INSTALL_MARKER_FILE);
|
|
2693
2820
|
}
|
|
@@ -2854,6 +2981,20 @@ function recordInstallFailure(dir, input) {
|
|
|
2854
2981
|
fs.renameSync(temp, target2);
|
|
2855
2982
|
return record;
|
|
2856
2983
|
}
|
|
2984
|
+
function installAttemptSupersededBy(record, runningVersion) {
|
|
2985
|
+
const standing = targetStanding(record.targetVersion, runningVersion);
|
|
2986
|
+
return standing === "reached" || standing === "passed";
|
|
2987
|
+
}
|
|
2988
|
+
function clearLastInstallAttempt(dir) {
|
|
2989
|
+
const path2 = lastInstallAttemptPath(dir);
|
|
2990
|
+
if (!fs.existsSync(path2)) return false;
|
|
2991
|
+
try {
|
|
2992
|
+
fs.rmSync(path2, { force: true });
|
|
2993
|
+
return true;
|
|
2994
|
+
} catch {
|
|
2995
|
+
return false;
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2857
2998
|
const VERSION_TRIPLE_REGEX = /^v?(\d+)\.(\d+)\.(\d+)/;
|
|
2858
2999
|
function compareVersions(a, b) {
|
|
2859
3000
|
const parse = (value) => {
|
|
@@ -2870,14 +3011,20 @@ function compareVersions(a, b) {
|
|
|
2870
3011
|
}
|
|
2871
3012
|
return 0;
|
|
2872
3013
|
}
|
|
3014
|
+
function targetStanding(target2, runningVersion) {
|
|
3015
|
+
if (target2 === runningVersion) return "reached";
|
|
3016
|
+
const order = compareVersions(target2, runningVersion);
|
|
3017
|
+
if (order === null) return "unorderable";
|
|
3018
|
+
if (order < 0) return "passed";
|
|
3019
|
+
if (order > 0) return "ahead";
|
|
3020
|
+
return "same-triple";
|
|
3021
|
+
}
|
|
2873
3022
|
function evaluatePendingInstall(input) {
|
|
2874
3023
|
const { marker } = input;
|
|
2875
3024
|
if (marker == null) return { kind: "none" };
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
}
|
|
2879
|
-
const order = compareVersions(marker.targetVersion, input.runningVersion);
|
|
2880
|
-
if (order !== null && order < 0) return { kind: "stale", marker };
|
|
3025
|
+
const standing = targetStanding(marker.targetVersion, input.runningVersion);
|
|
3026
|
+
if (standing === "reached") return { kind: "succeeded", marker };
|
|
3027
|
+
if (standing === "passed") return { kind: "stale", marker };
|
|
2881
3028
|
if (input.installInFlight === true) return { kind: "in-flight", marker };
|
|
2882
3029
|
return { kind: "failed", marker };
|
|
2883
3030
|
}
|
|
@@ -3700,6 +3847,23 @@ function generationInstallRoot(identity) {
|
|
|
3700
3847
|
dir = parent;
|
|
3701
3848
|
}
|
|
3702
3849
|
}
|
|
3850
|
+
function installPointerPath(filePath) {
|
|
3851
|
+
if (!filePath) return null;
|
|
3852
|
+
const identity = readInstallIdentity(filePath);
|
|
3853
|
+
const generation = generationInstallRoot(identity);
|
|
3854
|
+
if (!generation) return filePath;
|
|
3855
|
+
let resolved;
|
|
3856
|
+
try {
|
|
3857
|
+
resolved = fs.realpathSync(identity.realPath ?? filePath);
|
|
3858
|
+
} catch {
|
|
3859
|
+
return filePath;
|
|
3860
|
+
}
|
|
3861
|
+
const inside2 = path$1.relative(generation, resolved);
|
|
3862
|
+
if (inside2 === "" || inside2.startsWith("..") || path$1.isAbsolute(inside2)) {
|
|
3863
|
+
return filePath;
|
|
3864
|
+
}
|
|
3865
|
+
return path$1.join(path$1.dirname(path$1.dirname(generation)), "current", inside2);
|
|
3866
|
+
}
|
|
3703
3867
|
function classifyGlobalInstall(identity) {
|
|
3704
3868
|
if (!identity.path) return "global-unknown";
|
|
3705
3869
|
if (identity.editable) return "editable";
|
|
@@ -4352,7 +4516,8 @@ async function requestDesktopMediaOutcome(input, bytes, backendUrl, token) {
|
|
|
4352
4516
|
response: {
|
|
4353
4517
|
status: 503,
|
|
4354
4518
|
kind: "error",
|
|
4355
|
-
detail:
|
|
4519
|
+
detail: DESKTOP_MACHINE_DETAIL.noCredential,
|
|
4520
|
+
code: DESKTOP_REFUSAL_CODE.noCredential
|
|
4356
4521
|
},
|
|
4357
4522
|
answered: false
|
|
4358
4523
|
};
|
|
@@ -4421,7 +4586,15 @@ async function requestDesktopMediaOutcome(input, bytes, backendUrl, token) {
|
|
|
4421
4586
|
}
|
|
4422
4587
|
}
|
|
4423
4588
|
return {
|
|
4424
|
-
response: {
|
|
4589
|
+
response: {
|
|
4590
|
+
status: response.status,
|
|
4591
|
+
kind: "error",
|
|
4592
|
+
detail,
|
|
4593
|
+
// Only a path the plane has to ADMIT makes the status a pairing fact:
|
|
4594
|
+
// `speech.create` and `agent.export` are outside `/v1/desktop/`, where a
|
|
4595
|
+
// 503 is that route's own failure and not this app's pairing.
|
|
4596
|
+
code: desktopRefusalCodeForStatus(target2.path, response.status)
|
|
4597
|
+
},
|
|
4425
4598
|
answered: true
|
|
4426
4599
|
};
|
|
4427
4600
|
}
|
|
@@ -4461,7 +4634,8 @@ async function requestDesktopMediaOutcome(input, bytes, backendUrl, token) {
|
|
|
4461
4634
|
response: {
|
|
4462
4635
|
status: 503,
|
|
4463
4636
|
kind: "error",
|
|
4464
|
-
detail:
|
|
4637
|
+
detail: DESKTOP_MACHINE_DETAIL.transportFailed,
|
|
4638
|
+
code: DESKTOP_REFUSAL_CODE.transportFailed
|
|
4465
4639
|
},
|
|
4466
4640
|
/*
|
|
4467
4641
|
* Survives the catch for the same reason as its JSON twin, and with the same
|
|
@@ -4705,7 +4879,10 @@ async function requestDesktopOutcome(input, backendUrl, token) {
|
|
|
4705
4879
|
response: {
|
|
4706
4880
|
status: 503,
|
|
4707
4881
|
body: {
|
|
4708
|
-
detail:
|
|
4882
|
+
detail: {
|
|
4883
|
+
code: DESKTOP_REFUSAL_CODE.noCredential,
|
|
4884
|
+
message: DESKTOP_MACHINE_DETAIL.noCredential
|
|
4885
|
+
}
|
|
4709
4886
|
}
|
|
4710
4887
|
},
|
|
4711
4888
|
answered: false
|
|
@@ -4767,7 +4944,10 @@ async function requestDesktopOutcome(input, backendUrl, token) {
|
|
|
4767
4944
|
response: {
|
|
4768
4945
|
status: 503,
|
|
4769
4946
|
body: {
|
|
4770
|
-
detail:
|
|
4947
|
+
detail: {
|
|
4948
|
+
code: DESKTOP_REFUSAL_CODE.transportFailed,
|
|
4949
|
+
message: DESKTOP_MACHINE_DETAIL.transportFailed
|
|
4950
|
+
}
|
|
4771
4951
|
}
|
|
4772
4952
|
},
|
|
4773
4953
|
/*
|
|
@@ -5177,7 +5357,14 @@ class DaemonStateMachine {
|
|
|
5177
5357
|
identity = null;
|
|
5178
5358
|
failures = 0;
|
|
5179
5359
|
capabilityStatus = null;
|
|
5180
|
-
|
|
5360
|
+
/**
|
|
5361
|
+
* Whether this app is paired with the daemon it can see, and why not when it
|
|
5362
|
+
* is not. ONE field rather than the pair it used to be (`desktopAvailable`,
|
|
5363
|
+
* written `true` at three sites and `false` at none): the value and its cause
|
|
5364
|
+
* are one fact, and a producer that can only set half of it is how the app
|
|
5365
|
+
* went on claiming a pairing a successor had already destroyed.
|
|
5366
|
+
*/
|
|
5367
|
+
pairing = DAEMON_UNPAIRED;
|
|
5181
5368
|
detail = "Looking for a Local Operator daemon.";
|
|
5182
5369
|
updatedAt;
|
|
5183
5370
|
detachedSince = null;
|
|
@@ -5196,6 +5383,7 @@ class DaemonStateMachine {
|
|
|
5196
5383
|
this.backoffMs = REATTACH_BACKOFF_MS;
|
|
5197
5384
|
this.state = "attached";
|
|
5198
5385
|
this.detail = `Connected to the daemon on ${identity.url} (pid ${identity.pid}, v${identity.version}).`;
|
|
5386
|
+
this.pairing = DAEMON_PAIRED;
|
|
5199
5387
|
this.updatedAt = this.now();
|
|
5200
5388
|
}
|
|
5201
5389
|
/** This app started a successor for a daemon it owns. */
|
|
@@ -5205,9 +5393,15 @@ class DaemonStateMachine {
|
|
|
5205
5393
|
this.detail = `Started a replacement daemon on ${identity.url} (pid ${identity.pid}).`;
|
|
5206
5394
|
this.updatedAt = this.now();
|
|
5207
5395
|
}
|
|
5208
|
-
/**
|
|
5209
|
-
|
|
5210
|
-
|
|
5396
|
+
/**
|
|
5397
|
+
* Record this app's PAIRING with the daemon, cause and all.
|
|
5398
|
+
*
|
|
5399
|
+
* The replacement for `setDesktopAvailable(boolean)`, which could only ever be
|
|
5400
|
+
* called with `true` in practice and therefore could not report the one thing
|
|
5401
|
+
* the renderer needed: which fact made the app unusable (design § 1.4, § 5.2).
|
|
5402
|
+
*/
|
|
5403
|
+
setPairing(pairing) {
|
|
5404
|
+
this.pairing = pairing;
|
|
5211
5405
|
this.updatedAt = this.now();
|
|
5212
5406
|
}
|
|
5213
5407
|
/**
|
|
@@ -5455,7 +5649,11 @@ class DaemonStateMachine {
|
|
|
5455
5649
|
version: this.identity?.version ?? null,
|
|
5456
5650
|
prefix: this.identity?.prefix ?? null,
|
|
5457
5651
|
installKind: this.identity?.installKind ?? null,
|
|
5458
|
-
|
|
5652
|
+
// DERIVED, never set independently: the pairing record is the value, and
|
|
5653
|
+
// this field is its boolean projection for the callers that only need the
|
|
5654
|
+
// bit. One assignment, so the two cannot disagree.
|
|
5655
|
+
desktopAvailable: this.pairing.available,
|
|
5656
|
+
pairing: this.pairing,
|
|
5459
5657
|
failures: this.failures,
|
|
5460
5658
|
capabilityStatus: this.capabilityStatus,
|
|
5461
5659
|
/**
|
|
@@ -6566,6 +6764,20 @@ class BackendServiceManager {
|
|
|
6566
6764
|
*/
|
|
6567
6765
|
feedRelay = null;
|
|
6568
6766
|
feedRelayUrl = "";
|
|
6767
|
+
/**
|
|
6768
|
+
* The credential the current feed relay was built with.
|
|
6769
|
+
*
|
|
6770
|
+
* WHY IT IS TRACKED AT ALL, and the defect it stands for: the relay takes its
|
|
6771
|
+
* bearer ONCE, at construction, and a re-pair leaves the address unchanged
|
|
6772
|
+
* while replacing the credential - same port, new claim key. A rebuild keyed on
|
|
6773
|
+
* the URL alone therefore keeps a relay that 401s on every attempt, for good:
|
|
6774
|
+
* measured after the successor swap, the app was `attached` and paired with the
|
|
6775
|
+
* new daemon while the sidebar still rendered "Not connected to the backend -
|
|
6776
|
+
* showing the last known state." more than a minute later, and the same line
|
|
6777
|
+
* never cleared (QA round 1 Q-2, design round 1 D3). The stream relay next door
|
|
6778
|
+
* already compares the token; this one now does too.
|
|
6779
|
+
*/
|
|
6780
|
+
feedRelayToken = null;
|
|
6569
6781
|
/** Both survive relay recreation, so a URL rotation cannot silently detach
|
|
6570
6782
|
* the banner path or leave the sidebar reading a stale connection state. */
|
|
6571
6783
|
feedFrameObserver = null;
|
|
@@ -6679,7 +6891,10 @@ class BackendServiceManager {
|
|
|
6679
6891
|
* backend-ready hook, which is the first moment the token exists.
|
|
6680
6892
|
*/
|
|
6681
6893
|
getDesktopFeedRelay() {
|
|
6682
|
-
if (!this.feedRelay ||
|
|
6894
|
+
if (!this.feedRelay || relayNeedsRebuild(
|
|
6895
|
+
{ url: this.feedRelayUrl, token: this.feedRelayToken },
|
|
6896
|
+
{ url: this.backendUrl, token: this.desktopToken }
|
|
6897
|
+
)) {
|
|
6683
6898
|
this.feedRelay?.stop();
|
|
6684
6899
|
this.feedRelay = new DesktopFeedRelay(
|
|
6685
6900
|
this.backendUrl,
|
|
@@ -6703,6 +6918,7 @@ class BackendServiceManager {
|
|
|
6703
6918
|
this.feedRelay.observe(this.feedFrameObserver);
|
|
6704
6919
|
this.feedRelay.watchState(this.feedStateObserver);
|
|
6705
6920
|
this.feedRelayUrl = this.backendUrl;
|
|
6921
|
+
this.feedRelayToken = this.desktopToken;
|
|
6706
6922
|
}
|
|
6707
6923
|
return this.feedRelay;
|
|
6708
6924
|
}
|
|
@@ -7299,14 +7515,19 @@ class BackendServiceManager {
|
|
|
7299
7515
|
async attachIfUsable(candidate) {
|
|
7300
7516
|
const key = candidate.record.claim_key;
|
|
7301
7517
|
if (!key && !this.desktopToken) {
|
|
7518
|
+
const governed = candidate.record.desktop;
|
|
7302
7519
|
logger.info(
|
|
7303
|
-
`Daemon ${candidate.address} publishes no claim key and this app holds no pairing token for it; not attaching (its controls would refuse every call)
|
|
7520
|
+
`Daemon ${candidate.address} publishes no claim key and this app holds no pairing token for it; not attaching (its controls would refuse every call).${governed ? " Its record says the plane is already governed by another program." : " Its record does not describe a governed plane, so it predates the pairing handshake."}`,
|
|
7304
7521
|
LogFileType.BACKEND
|
|
7305
7522
|
);
|
|
7523
|
+
this.daemonState.setPairing({
|
|
7524
|
+
available: false,
|
|
7525
|
+
cause: governed ? "governed-elsewhere" : "pre-handshake"
|
|
7526
|
+
});
|
|
7306
7527
|
this.daemonState.observe({
|
|
7307
7528
|
kind: "capability",
|
|
7308
7529
|
status: 401,
|
|
7309
|
-
detail: `A daemon is running at ${candidate.address}, but this app holds no credential for
|
|
7530
|
+
detail: governed ? `A daemon is running at ${candidate.address}, but its desktop plane is already managed by another program and this app holds no credential for it.` : `A daemon is running at ${candidate.address}, but it is older than the pairing handshake this app uses and this app holds no credential for it.`
|
|
7310
7531
|
});
|
|
7311
7532
|
this.notifyStatus();
|
|
7312
7533
|
return false;
|
|
@@ -7330,11 +7551,40 @@ class BackendServiceManager {
|
|
|
7330
7551
|
);
|
|
7331
7552
|
break;
|
|
7332
7553
|
case "wrong-key":
|
|
7554
|
+
logger.info(
|
|
7555
|
+
`Daemon ${candidate.address} did not accept the claim key its own record published (${outcome.outcome}); the record belongs to a process that is no longer answering. Not attaching.`,
|
|
7556
|
+
LogFileType.BACKEND
|
|
7557
|
+
);
|
|
7558
|
+
this.daemonState.setPairing({
|
|
7559
|
+
available: false,
|
|
7560
|
+
cause: "successor"
|
|
7561
|
+
});
|
|
7562
|
+
return false;
|
|
7563
|
+
case "no-handshake":
|
|
7564
|
+
logger.info(
|
|
7565
|
+
`Daemon ${candidate.address} has no claim route (HTTP ${outcome.status}); it predates the pairing handshake. Not attaching.`,
|
|
7566
|
+
LogFileType.BACKEND
|
|
7567
|
+
);
|
|
7568
|
+
this.daemonState.setPairing({
|
|
7569
|
+
available: false,
|
|
7570
|
+
cause: "pre-handshake"
|
|
7571
|
+
});
|
|
7572
|
+
this.daemonState.observe({
|
|
7573
|
+
kind: "capability",
|
|
7574
|
+
status: outcome.status,
|
|
7575
|
+
detail: `A daemon is running at ${candidate.address}, but it is older than the pairing handshake this app uses (its claim route answered HTTP ${outcome.status}).`
|
|
7576
|
+
});
|
|
7577
|
+
this.notifyStatus();
|
|
7578
|
+
return false;
|
|
7333
7579
|
case "refused":
|
|
7334
7580
|
logger.info(
|
|
7335
|
-
`Daemon ${candidate.address} did not accept this app's claim (${outcome.outcome}); not attaching.`,
|
|
7581
|
+
`Daemon ${candidate.address} did not accept this app's claim (${outcome.outcome} ${outcome.status}); not attaching.`,
|
|
7336
7582
|
LogFileType.BACKEND
|
|
7337
7583
|
);
|
|
7584
|
+
this.daemonState.setPairing({
|
|
7585
|
+
available: false,
|
|
7586
|
+
cause: outcome.status === 503 ? "pre-handshake" : "credential-refused"
|
|
7587
|
+
});
|
|
7338
7588
|
return false;
|
|
7339
7589
|
case "unreachable":
|
|
7340
7590
|
logger.info(
|
|
@@ -7350,6 +7600,10 @@ class BackendServiceManager {
|
|
|
7350
7600
|
`Daemon ${candidate.address} refused this app's bearer for its desktop plane (HTTP ${probe.status}); not attaching (it would refuse every session list and every stream).`,
|
|
7351
7601
|
LogFileType.BACKEND
|
|
7352
7602
|
);
|
|
7603
|
+
this.daemonState.setPairing({
|
|
7604
|
+
available: false,
|
|
7605
|
+
cause: "credential-refused"
|
|
7606
|
+
});
|
|
7353
7607
|
this.daemonState.observe({
|
|
7354
7608
|
kind: "capability",
|
|
7355
7609
|
status: probe.status,
|
|
@@ -7438,7 +7692,7 @@ class BackendServiceManager {
|
|
|
7438
7692
|
},
|
|
7439
7693
|
{ owned: false }
|
|
7440
7694
|
);
|
|
7441
|
-
this.daemonState.
|
|
7695
|
+
this.daemonState.setPairing(DAEMON_PAIRED);
|
|
7442
7696
|
if (candidate.record.retiring_to) {
|
|
7443
7697
|
this.daemonState.observe({
|
|
7444
7698
|
kind: "build-announced",
|
|
@@ -7529,7 +7783,7 @@ class BackendServiceManager {
|
|
|
7529
7783
|
},
|
|
7530
7784
|
{ owned: false }
|
|
7531
7785
|
);
|
|
7532
|
-
this.daemonState.
|
|
7786
|
+
this.daemonState.setPairing(DAEMON_PAIRED);
|
|
7533
7787
|
logger.info(
|
|
7534
7788
|
`Adopted a pre-record daemon at ${this.backendUrl} (v${version || "unknown"}) through the deprecated fallback.`,
|
|
7535
7789
|
LogFileType.BACKEND
|
|
@@ -7806,7 +8060,7 @@ class BackendServiceManager {
|
|
|
7806
8060
|
return;
|
|
7807
8061
|
}
|
|
7808
8062
|
this.daemonState.attach(identity, { owned: true });
|
|
7809
|
-
this.daemonState.
|
|
8063
|
+
this.daemonState.setPairing(DAEMON_PAIRED);
|
|
7810
8064
|
this.notifyStatus();
|
|
7811
8065
|
logger.info(
|
|
7812
8066
|
`Registered this app's own daemon: ${identity.url} (pid ${identity.pid}, v${identity.version || "unknown"}, ${identity.installKind || "kind unknown"}).`,
|
|
@@ -8259,6 +8513,16 @@ class BackendServiceManager {
|
|
|
8259
8513
|
const probe = await probeIdentity(this.backendUrl, expected, {
|
|
8260
8514
|
timeoutMs: PROBE_TIMEOUT_MS
|
|
8261
8515
|
});
|
|
8516
|
+
const answersThisApp = probe.outcome === "identified" && probe.identity.pid === this.daemonState.snapshot().pid;
|
|
8517
|
+
const answeredAnotherProcess = probe.outcome === "identity-mismatch" || probe.outcome === "not-a-daemon" || probe.outcome === "identified";
|
|
8518
|
+
if (answersThisApp) {
|
|
8519
|
+
this.daemonState.setPairing(DAEMON_PAIRED);
|
|
8520
|
+
} else if (answeredAnotherProcess) {
|
|
8521
|
+
this.daemonState.setPairing({
|
|
8522
|
+
available: false,
|
|
8523
|
+
cause: "successor"
|
|
8524
|
+
});
|
|
8525
|
+
}
|
|
8262
8526
|
switch (probe.outcome) {
|
|
8263
8527
|
case "identified":
|
|
8264
8528
|
return probe.identity.pid === this.daemonState.snapshot().pid ? { kind: "identified" } : {
|
|
@@ -17618,6 +17882,7 @@ const INSTALL_IN_FLIGHT_RECHECK_MS = 1e4;
|
|
|
17618
17882
|
const LAUNCHCTL_NOT_LOADED_REGEX = /could not find|no such process/i;
|
|
17619
17883
|
const APP_OWNED_PUBLISH_HEADROOM_BYTES = 450 * 1024 * 1024;
|
|
17620
17884
|
const SEAL_PROBE_TIMEOUT_MS = 45e3;
|
|
17885
|
+
const STAGED_SIGNATURE_PROBE_TIMEOUT_MS = 2e4;
|
|
17621
17886
|
const STARTUP_SEAL_PROBE_DELAY_MS = 15e3;
|
|
17622
17887
|
const POST_WAKE_CHECK_DELAY_MS = 2e4;
|
|
17623
17888
|
function versionSuffix(version) {
|
|
@@ -18310,6 +18575,7 @@ class UpdateService {
|
|
|
18310
18575
|
);
|
|
18311
18576
|
this.reapWatchdog(outcome.marker, true);
|
|
18312
18577
|
clearPendingInstallMarker(this.markerDir());
|
|
18578
|
+
this.retireSupersededInstallRecord();
|
|
18313
18579
|
return;
|
|
18314
18580
|
case "stale":
|
|
18315
18581
|
logger.info(
|
|
@@ -18318,6 +18584,7 @@ class UpdateService {
|
|
|
18318
18584
|
);
|
|
18319
18585
|
this.reapWatchdog(outcome.marker, true);
|
|
18320
18586
|
clearPendingInstallMarker(this.markerDir());
|
|
18587
|
+
this.retireSupersededInstallRecord();
|
|
18321
18588
|
return;
|
|
18322
18589
|
case "in-flight": {
|
|
18323
18590
|
this.installWasInFlight = true;
|
|
@@ -19094,10 +19361,123 @@ ${heal.removed.join("\n")}`,
|
|
|
19094
19361
|
`Staged artifact verified: ${artifactPath} (${verdict.size} bytes, sha512 matches, ${required} bytes required free, ${installedBundleSize ?? "unknown"} byte app).`,
|
|
19095
19362
|
LogFileType.UPDATE_SERVICE
|
|
19096
19363
|
);
|
|
19097
|
-
return
|
|
19364
|
+
return this.stagedSignatureBlock(artifactPath, info);
|
|
19098
19365
|
}
|
|
19099
19366
|
return verdict.block;
|
|
19100
19367
|
}
|
|
19368
|
+
/**
|
|
19369
|
+
* Refuse a staged artifact macOS would not launch, before the app quits.
|
|
19370
|
+
*
|
|
19371
|
+
* Why this is worth a probe of its own. On 2026-09-19 the operator's install
|
|
19372
|
+
* updated itself to 0.29.6, which was Developer-ID signed, notarized, stapled,
|
|
19373
|
+
* `codesign --verify --deep --strict`-clean and `spctl -t exec`-accepted - and
|
|
19374
|
+
* completely unlaunchable: every executable in the bundle claimed the
|
|
19375
|
+
* restricted `keychain-access-groups` entitlement while the bundle embedded no
|
|
19376
|
+
* provisioning profile to authorize it, so amfid refused every Mach-O at exec
|
|
19377
|
+
* (`AppleMobileFileIntegrityError Code=-413 "No matching profile found"`).
|
|
19378
|
+
* `open` reported `RBSRequestErrorDomain Code=5 / NSPOSIXErrorDomain Code=163
|
|
19379
|
+
* "Launchd job spawn failed"`. The ShipIt that should have relaunched the app
|
|
19380
|
+
* was refused for the same reason, so the install completed and left the user
|
|
19381
|
+
* with no app, no message and no automatic way back.
|
|
19382
|
+
*
|
|
19383
|
+
* The signature check is done on the STAGED artifact rather than on the
|
|
19384
|
+
* installed bundle because by the time the installed bundle is the broken one,
|
|
19385
|
+
* there is nothing left running to report it.
|
|
19386
|
+
*
|
|
19387
|
+
* A probe that could not read the artifact at all is not a refusal by itself -
|
|
19388
|
+
* `stagedSignatureBlock` decides that - and neither is a non-zip artifact: the
|
|
19389
|
+
* macOS in-app update is a `.zip`, and an artifact this cannot read is reported
|
|
19390
|
+
* as such rather than treated as clean.
|
|
19391
|
+
*/
|
|
19392
|
+
async stagedSignatureBlock(artifactPath, info) {
|
|
19393
|
+
const facts = await this.inspectStagedSignature(artifactPath, info);
|
|
19394
|
+
if (!facts) return null;
|
|
19395
|
+
const block = stagedSignatureBlock(facts);
|
|
19396
|
+
if (!block) {
|
|
19397
|
+
logger.info(
|
|
19398
|
+
`Staged artifact signature checked: ${facts.artifactName} claims no entitlement macOS needs a provisioning profile for.`,
|
|
19399
|
+
LogFileType.UPDATE_SERVICE
|
|
19400
|
+
);
|
|
19401
|
+
return null;
|
|
19402
|
+
}
|
|
19403
|
+
logger.warn(
|
|
19404
|
+
`Refusing the update to ${info?.version ?? "an unknown version"}: ${block.detail}`,
|
|
19405
|
+
LogFileType.UPDATE_SERVICE
|
|
19406
|
+
);
|
|
19407
|
+
return block;
|
|
19408
|
+
}
|
|
19409
|
+
/**
|
|
19410
|
+
* The facts `stagedSignatureBlock` decides on, read out of the archive.
|
|
19411
|
+
*
|
|
19412
|
+
* Two members of the zip and one `codesign`, which is cheap because the file
|
|
19413
|
+
* that carries the entitlements is the bundle's LAUNCHER rather than the app
|
|
19414
|
+
* (`Contents/MacOS/Local Operator` is ~54 KB beside a ~376 MB bundle). The
|
|
19415
|
+
* listing comes from the central directory, so neither the interpreter nor the
|
|
19416
|
+
* frameworks are ever decompressed.
|
|
19417
|
+
*
|
|
19418
|
+
* Returns null when there is nothing this can ask about - not a zip, or a zip
|
|
19419
|
+
* whose listing could not be read - and says so in the log. That is a
|
|
19420
|
+
* deliberate "we could not tell", which is different from "we looked and it is
|
|
19421
|
+
* clean"; the caller logs the clean case itself.
|
|
19422
|
+
*/
|
|
19423
|
+
async inspectStagedSignature(artifactPath, info) {
|
|
19424
|
+
const artifactName = path$1.basename(artifactPath);
|
|
19425
|
+
if (!artifactPath.toLowerCase().endsWith(".zip")) {
|
|
19426
|
+
logger.info(
|
|
19427
|
+
`Staged artifact ${artifactName} is not a zip; its signature was not inspected before the install.`,
|
|
19428
|
+
LogFileType.UPDATE_SERVICE
|
|
19429
|
+
);
|
|
19430
|
+
return null;
|
|
19431
|
+
}
|
|
19432
|
+
const listing = await runCommand("/usr/bin/unzip", ["-Z1", artifactPath], {
|
|
19433
|
+
timeoutMs: STAGED_SIGNATURE_PROBE_TIMEOUT_MS
|
|
19434
|
+
});
|
|
19435
|
+
if (!listing.ran || listing.exitCode !== 0) {
|
|
19436
|
+
logger.warn(
|
|
19437
|
+
`Could not list ${artifactName} to check its signature (exit ${listing.exitCode}): ${listing.stderr.trim() || "no output"}`,
|
|
19438
|
+
LogFileType.UPDATE_SERVICE
|
|
19439
|
+
);
|
|
19440
|
+
return null;
|
|
19441
|
+
}
|
|
19442
|
+
const embeddedProfile = zipListingHasEmbeddedProfile(listing.stdout);
|
|
19443
|
+
const entry = zipMainExecutableEntry(listing.stdout);
|
|
19444
|
+
const base = {
|
|
19445
|
+
entitlementsPlist: null,
|
|
19446
|
+
embeddedProfile,
|
|
19447
|
+
artifactName,
|
|
19448
|
+
version: info?.version ?? null
|
|
19449
|
+
};
|
|
19450
|
+
if (!entry) {
|
|
19451
|
+
logger.warn(
|
|
19452
|
+
`${artifactName} has no Contents/MacOS entry to read a signature from; its entitlements were not inspected before the install.`,
|
|
19453
|
+
LogFileType.UPDATE_SERVICE
|
|
19454
|
+
);
|
|
19455
|
+
return base;
|
|
19456
|
+
}
|
|
19457
|
+
const scratch = fs.mkdtempSync(path$1.join(os.tmpdir(), "lo-staged-signature-"));
|
|
19458
|
+
try {
|
|
19459
|
+
const extracted = await runCommand(
|
|
19460
|
+
"/usr/bin/unzip",
|
|
19461
|
+
["-o", "-q", artifactPath, entry, "-d", scratch],
|
|
19462
|
+
{ timeoutMs: STAGED_SIGNATURE_PROBE_TIMEOUT_MS }
|
|
19463
|
+
);
|
|
19464
|
+
if (!extracted.ran || extracted.exitCode !== 0) {
|
|
19465
|
+
logger.warn(
|
|
19466
|
+
`Could not extract ${entry} from ${artifactName} (exit ${extracted.exitCode}): ${extracted.stderr.trim() || "no output"}`,
|
|
19467
|
+
LogFileType.UPDATE_SERVICE
|
|
19468
|
+
);
|
|
19469
|
+
return base;
|
|
19470
|
+
}
|
|
19471
|
+
const signature = await runCommand(
|
|
19472
|
+
"/usr/bin/codesign",
|
|
19473
|
+
["-d", "--entitlements", "-", "--xml", path$1.join(scratch, entry)],
|
|
19474
|
+
{ timeoutMs: STAGED_SIGNATURE_PROBE_TIMEOUT_MS }
|
|
19475
|
+
);
|
|
19476
|
+
return signature.ran && signature.exitCode === 0 ? { ...base, entitlementsPlist: signature.stdout } : base;
|
|
19477
|
+
} finally {
|
|
19478
|
+
fs.rmSync(scratch, { recursive: true, force: true });
|
|
19479
|
+
}
|
|
19480
|
+
}
|
|
19101
19481
|
/**
|
|
19102
19482
|
* Everything that has to hold before the app is allowed to quit.
|
|
19103
19483
|
*
|
|
@@ -19315,9 +19695,39 @@ ${heal.removed.join("\n")}`,
|
|
|
19315
19695
|
* a dismiss: the panel that reports a failure is not the only place the user
|
|
19316
19696
|
* can find out what happened, which is what made Dismiss an information loss
|
|
19317
19697
|
* (reviews U1, D3).
|
|
19698
|
+
*
|
|
19699
|
+
* AND IT RETIRES ONCE THE MACHINE HAS ARRIVED, which is the other half of that
|
|
19700
|
+
* promise: a record that survives a dismiss must not survive the state it
|
|
19701
|
+
* describes, or "Version 0.28.2 is running" is printed to a user on 0.29.1 for
|
|
19702
|
+
* ever (the operator's report, 2026-09-18 - a 0.28.3 record against a running
|
|
19703
|
+
* 0.29.1). The test is read-time because the fact becomes true without this
|
|
19704
|
+
* process doing anything: the installer ran, the user relaunched, and the app
|
|
19705
|
+
* that comes up is the one that answers every reader.
|
|
19318
19706
|
*/
|
|
19319
19707
|
lastInstallAttempt() {
|
|
19320
|
-
return
|
|
19708
|
+
return this.retireSupersededInstallRecord();
|
|
19709
|
+
}
|
|
19710
|
+
/**
|
|
19711
|
+
* The record, retired first when the version now running has reached its target.
|
|
19712
|
+
*
|
|
19713
|
+
* One function rather than the same test written at each site, because there are two
|
|
19714
|
+
* moments the fact becomes true - the launch that reads the record, and a start-up
|
|
19715
|
+
* that observes an install having succeeded or been superseded - and two spellings of
|
|
19716
|
+
* one rule is how the arms drift apart. The predicate keeps a record whose versions
|
|
19717
|
+
* cannot be ordered, so nothing is dropped on a guess (`installAttemptSupersededBy`).
|
|
19718
|
+
*/
|
|
19719
|
+
retireSupersededInstallRecord() {
|
|
19720
|
+
const record = readLastInstallAttempt(this.markerDir());
|
|
19721
|
+
if (!record) return null;
|
|
19722
|
+
const running = electron.app.getVersion();
|
|
19723
|
+
if (!installAttemptSupersededBy(record, running)) return record;
|
|
19724
|
+
if (clearLastInstallAttempt(this.markerDir())) {
|
|
19725
|
+
logger.info(
|
|
19726
|
+
`Retired the recorded install failure for ${record.targetVersion}: this app is running ${running}, so the target has been reached and the record describes a state the machine has left`,
|
|
19727
|
+
LogFileType.UPDATE_SERVICE
|
|
19728
|
+
);
|
|
19729
|
+
}
|
|
19730
|
+
return null;
|
|
19321
19731
|
}
|
|
19322
19732
|
/**
|
|
19323
19733
|
* Note that a check was asked for, and let an explicit one re-offer a release
|
|
@@ -20155,9 +20565,15 @@ ${heal.removed.join("\n")}`,
|
|
|
20155
20565
|
* uv tool root), so a prefix held from minutes ago can name a tree nothing is
|
|
20156
20566
|
* using any more. The reading is what `didSourceRebuildLand` compares, in both
|
|
20157
20567
|
* directions.
|
|
20568
|
+
*
|
|
20569
|
+
* Read through the install's own pointer for the same reason the version is (see
|
|
20570
|
+
* `readInstallVersionAt`): a rebuild keeps `pyproject.toml`'s version and moves the
|
|
20571
|
+
* `.lop-source` ref, so the marker is the ONLY evidence of that move on this route -
|
|
20572
|
+
* and a marker read from the frozen `generations/<id>` the daemon started from cannot
|
|
20573
|
+
* show it any more than the version could.
|
|
20158
20574
|
*/
|
|
20159
20575
|
readRebuildMarkerState(installPath) {
|
|
20160
|
-
const identity = readInstallIdentity(installPath);
|
|
20576
|
+
const identity = readInstallIdentity(installPointerPath(installPath));
|
|
20161
20577
|
return readSourceMarkerState(identity.venvPrefix ?? null);
|
|
20162
20578
|
}
|
|
20163
20579
|
/**
|
|
@@ -20943,9 +21359,16 @@ ${heal.removed.join("\n")}`,
|
|
|
20943
21359
|
* than into a plausible number. The one caller with a record and no path - the
|
|
20944
21360
|
* reconciliation reading a marker an older build wrote - names its fallback explicitly
|
|
20945
21361
|
* and logs it.
|
|
21362
|
+
*
|
|
21363
|
+
* AND THE INSTALL IS READ THROUGH ITS OWN POINTER, because on a generation layout the
|
|
21364
|
+
* install it names cannot move under it: the tree `/health` reports as the install root
|
|
21365
|
+
* is `generations/<id>`, an install lands BESIDE it, and only `<stable>/current` moves.
|
|
21366
|
+
* Reading the concrete path here is what produced the false "did not take effect" verdict
|
|
21367
|
+
* of 2026-09-18; `installPointerPath` is the same install named so the flip is visible,
|
|
21368
|
+
* and it is a no-op on every other layout.
|
|
20946
21369
|
*/
|
|
20947
21370
|
readInstallVersionAt(installPath) {
|
|
20948
|
-
return readInstallIdentity(installPath)?.version ?? null;
|
|
21371
|
+
return readInstallIdentity(installPointerPath(installPath))?.version ?? null;
|
|
20949
21372
|
}
|
|
20950
21373
|
/**
|
|
20951
21374
|
* Run the install's own front end: `<resolved shim> update`.
|
|
@@ -21175,8 +21598,17 @@ ${heal.removed.join("\n")}`,
|
|
|
21175
21598
|
* The install this attempt runs, recorded so a later launch does not have to resolve
|
|
21176
21599
|
* one of its own: the reconciliation reads this, and a marker without it (an older
|
|
21177
21600
|
* build's) is the only case that falls back.
|
|
21601
|
+
*
|
|
21602
|
+
* RECORDED THROUGH ITS OWN POINTER where the layout has one, so the record outlives
|
|
21603
|
+
* the tree it names: on a generation install the flip an install performs is visible
|
|
21604
|
+
* only through `<stable>/current`, and the generation this attempt started from is
|
|
21605
|
+
* unreferenced the moment it is superseded - which makes it prunable, and a record
|
|
21606
|
+
* naming a pruned tree reads as "nothing moved" for an install that landed. A record
|
|
21607
|
+
* an older build wrote carries the concrete generation path instead, which the reader
|
|
21608
|
+
* resolves through the same pointer (`installPointerPath` is idempotent, so both
|
|
21609
|
+
* spellings land in the same place).
|
|
21178
21610
|
*/
|
|
21179
|
-
installPath
|
|
21611
|
+
installPath: installPointerPath(installPath)
|
|
21180
21612
|
};
|
|
21181
21613
|
writePendingServerUpdateMarker(this.markerDir(), marker);
|
|
21182
21614
|
const run = await this.runGlobalUpdate(
|
|
@@ -21198,7 +21630,7 @@ ${heal.removed.join("\n")}`,
|
|
|
21198
21630
|
deadlineAt: marker.deadlineAt,
|
|
21199
21631
|
groupPid: pid,
|
|
21200
21632
|
groupStartedAt: readProcessStartStamp(pid),
|
|
21201
|
-
installPath
|
|
21633
|
+
installPath: installPointerPath(installPath)
|
|
21202
21634
|
});
|
|
21203
21635
|
}
|
|
21204
21636
|
);
|