@ours.network/install 1.2.1-nightly.7 → 1.2.1-nightly.9

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/README.md CHANGED
@@ -295,3 +295,22 @@ deletion is required; repeat the original setup with the updated installer.
295
295
  The repair runs isolated verification containers without state mounts or network
296
296
  access. Other image verification failures stop setup without replacing the image.
297
297
  An interrupted repair can be retried, including after the image tag was replaced.
298
+
299
+ ### Remote clients over HTTPS
300
+
301
+ A client may select an `https://` origin backed by a TLS reverse proxy. Keep the
302
+ daemon listener private and configure the proxy separately with a certificate
303
+ trusted by the client and matching the hostname. Node uses its normal trust
304
+ store; a private CA may be supplied through `NODE_EXTRA_CA_CERTS` before starting
305
+ the client. Certificate verification must remain enabled.
306
+
307
+ The existing issued client credential works over either transport; changing the
308
+ URL scheme does not require a new credential. Keep the server's master on the
309
+ server. Forward `x-ours-api-token` and the `x-ours-*` session headers unchanged,
310
+ and support streamed request/response bodies and long polling. Client requests
311
+ refuse redirects, including HTTPS-to-HTTP redirects: configure the final HTTPS
312
+ origin directly. UUID/capability checks still precede credential-bearing calls.
313
+
314
+ This adds client HTTPS support, not an HTTPS daemon listener, certificate
315
+ provisioning or automatic reverse-proxy configuration. Existing local HTTP and
316
+ SSH-tunnel profiles continue to work.
@@ -8,15 +8,15 @@
8
8
  "name": "ours-release-set-verification",
9
9
  "version": "0.0.0",
10
10
  "dependencies": {
11
- "@ours.network/claude-code": "1.2.0-nightly.3",
12
- "@ours.network/cli": "2.8.1-nightly.1",
13
- "@ours.network/codex": "1.2.0-nightly.3",
14
- "@ours.network/cowork": "1.3.2-nightly.20260918.4d9242e",
15
- "@ours.network/fleet": "1.2.0-nightly.1",
16
- "@ours.network/mcp": "1.2.0-nightly.3",
17
- "@ours.network/messenger-server": "1.0.31-nightly.1",
18
- "@ours.network/sdk": "3.8.1-nightly.3",
19
- "@ours.network/tg-connector": "1.0.1-nightly.1"
11
+ "@ours.network/claude-code": "1.2.0-nightly.4",
12
+ "@ours.network/cli": "2.8.1-nightly.2",
13
+ "@ours.network/codex": "1.2.0-nightly.4",
14
+ "@ours.network/cowork": "1.3.2-nightly.20260919.5e6e5fd",
15
+ "@ours.network/fleet": "1.2.0-nightly.2",
16
+ "@ours.network/mcp": "1.2.0-nightly.4",
17
+ "@ours.network/messenger-server": "1.0.31-nightly.2",
18
+ "@ours.network/sdk": "3.8.1-nightly.4",
19
+ "@ours.network/tg-connector": "1.0.1-nightly.2"
20
20
  }
21
21
  },
22
22
  "node_modules/@adapt-toolkit/sdk": {
@@ -1051,13 +1051,13 @@
1051
1051
  }
1052
1052
  },
1053
1053
  "node_modules/@ours.network/claude-code": {
1054
- "version": "1.2.0-nightly.3",
1055
- "resolved": "https://registry.npmjs.org/@ours.network/claude-code/-/claude-code-1.2.0-nightly.3.tgz",
1056
- "integrity": "sha512-7ta6KnvoHB2i/yoaykhXvzKFQTIOPtDHeTzG1PRwHjdTJ4vcGEh4jWYMd2c1jjj73lC3q9qW3LYSRutIJovtQg==",
1054
+ "version": "1.2.0-nightly.4",
1055
+ "resolved": "https://registry.npmjs.org/@ours.network/claude-code/-/claude-code-1.2.0-nightly.4.tgz",
1056
+ "integrity": "sha512-JNDhTMtxO2Xm0NNL/5Kgra/ovr62mP0YFcuvYJRRA+SH6+2Ick8eegeSVmzU97HPLBCTi8DwNqfkUxcCQUpwXA==",
1057
1057
  "license": "FSL-1.1-Apache-2.0",
1058
1058
  "dependencies": {
1059
1059
  "@modelcontextprotocol/sdk": "^1.29.0",
1060
- "@ours.network/sdk": "3.8.1-nightly.3",
1060
+ "@ours.network/sdk": "3.8.1-nightly.4",
1061
1061
  "ws": "^8.21.0",
1062
1062
  "zod": "^3.25.76"
1063
1063
  },
@@ -1066,14 +1066,14 @@
1066
1066
  }
1067
1067
  },
1068
1068
  "node_modules/@ours.network/cli": {
1069
- "version": "2.8.1-nightly.1",
1070
- "resolved": "https://registry.npmjs.org/@ours.network/cli/-/cli-2.8.1-nightly.1.tgz",
1071
- "integrity": "sha512-1Rmbqlr790FhqQW/wQ//YEL7b0Q2KVSCA2TlpjaLRkj9Xh55rOoazBJjFuW/fUpTz3Z1B3MUPK9LWiYd+A0kNA==",
1069
+ "version": "2.8.1-nightly.2",
1070
+ "resolved": "https://registry.npmjs.org/@ours.network/cli/-/cli-2.8.1-nightly.2.tgz",
1071
+ "integrity": "sha512-YirjiR6/Xs1p2LKOLVtao74xLXCsxgHz3gwbKyZ4Qtd9w6MF+kS/OEOLKozmIjWJ5FoqUxgvnnaudl5vhaeUkA==",
1072
1072
  "license": "FSL-1.1-Apache-2.0",
1073
1073
  "dependencies": {
1074
1074
  "@adapt-toolkit/sdk": "0.10.13-nightly.1",
1075
1075
  "@adapt-toolkit/sdk-native": "0.10.13-nightly.1",
1076
- "@ours.network/sdk": "3.8.1-nightly.3",
1076
+ "@ours.network/sdk": "3.8.1-nightly.4",
1077
1077
  "pg": "^8.16.3"
1078
1078
  },
1079
1079
  "bin": {
@@ -1087,13 +1087,13 @@
1087
1087
  }
1088
1088
  },
1089
1089
  "node_modules/@ours.network/codex": {
1090
- "version": "1.2.0-nightly.3",
1091
- "resolved": "https://registry.npmjs.org/@ours.network/codex/-/codex-1.2.0-nightly.3.tgz",
1092
- "integrity": "sha512-dIWDqbo6hJzujegHRO6x1SsBnh5p1c6gRGj/WkPha/hcN7s/27P4jqBnJ7czQyVGiNVFZ1aAxj4c9+CDLzHENw==",
1090
+ "version": "1.2.0-nightly.4",
1091
+ "resolved": "https://registry.npmjs.org/@ours.network/codex/-/codex-1.2.0-nightly.4.tgz",
1092
+ "integrity": "sha512-ZYzqWWcPv9snZH86AZxGfkT2lc9djIJuvBBWdDC7j7YJRjXVQfTVGNFtc5NoGesQYiiQFhiumSppMxfc3vHVDA==",
1093
1093
  "license": "FSL-1.1-Apache-2.0",
1094
1094
  "dependencies": {
1095
1095
  "@modelcontextprotocol/sdk": "^1.29.0",
1096
- "@ours.network/sdk": "3.8.1-nightly.3",
1096
+ "@ours.network/sdk": "3.8.1-nightly.4",
1097
1097
  "ws": "^8.21.0",
1098
1098
  "zod": "^3.25.76"
1099
1099
  },
@@ -1106,12 +1106,12 @@
1106
1106
  }
1107
1107
  },
1108
1108
  "node_modules/@ours.network/cowork": {
1109
- "version": "1.3.2-nightly.20260918.4d9242e",
1110
- "resolved": "https://registry.npmjs.org/@ours.network/cowork/-/cowork-1.3.2-nightly.20260918.4d9242e.tgz",
1111
- "integrity": "sha512-04D2lJ97SrTrkVRRbdTZ0y+KvQdG/bpnmFFjsyUqQTij+5zEkNizTG/xhcCL8m7Mb0XvVFL8WzJ62Xgh5ahlRw==",
1109
+ "version": "1.3.2-nightly.20260919.5e6e5fd",
1110
+ "resolved": "https://registry.npmjs.org/@ours.network/cowork/-/cowork-1.3.2-nightly.20260919.5e6e5fd.tgz",
1111
+ "integrity": "sha512-/3KEKXqriBsPiK8POyL/XgbP4ELsRs6quv0Q47cFCaesXAR4W44Q3NVs43brnj1AwMLroip0iXzqwlsJvpoaTw==",
1112
1112
  "license": "FSL-1.1-Apache-2.0",
1113
1113
  "dependencies": {
1114
- "@ours.network/sdk": "3.8.1-nightly.3",
1114
+ "@ours.network/sdk": "3.8.1-nightly.4",
1115
1115
  "ajv": "8.20.0",
1116
1116
  "better-sqlite3": "13.0.3",
1117
1117
  "react": "18.3.1",
@@ -1126,16 +1126,16 @@
1126
1126
  }
1127
1127
  },
1128
1128
  "node_modules/@ours.network/fleet": {
1129
- "version": "1.2.0-nightly.1",
1130
- "resolved": "https://registry.npmjs.org/@ours.network/fleet/-/fleet-1.2.0-nightly.1.tgz",
1131
- "integrity": "sha512-vioCobVbosmp9BgYo2XccjpPoVQd1wwV+Mt64Bm41/Vxtro2j85PUdy4AWcsY9jEFnJmj/UQfc5E5AxvlpPnrw==",
1129
+ "version": "1.2.0-nightly.2",
1130
+ "resolved": "https://registry.npmjs.org/@ours.network/fleet/-/fleet-1.2.0-nightly.2.tgz",
1131
+ "integrity": "sha512-vnTGjjl2ycd7FN4CKGblemFcq5/MWocTR0CQhGEbspYXRYiojGa3MRyLzJPOVJQbP0/zqIwSG5Vk9czMWHpjow==",
1132
1132
  "license": "FSL-1.1-Apache-2.0",
1133
1133
  "dependencies": {
1134
1134
  "@agentclientprotocol/sdk": "^1.3.0",
1135
1135
  "@fastify/static": "^10.1.2",
1136
1136
  "@fastify/websocket": "^11.2.0",
1137
- "@ours.network/cli": "2.8.1-nightly.1",
1138
- "@ours.network/sdk": "3.8.1-nightly.3",
1137
+ "@ours.network/cli": "2.8.1-nightly.2",
1138
+ "@ours.network/sdk": "3.8.1-nightly.4",
1139
1139
  "commander": "^12.1.0",
1140
1140
  "fastify": "^5.4.0",
1141
1141
  "react": "^19.1.1",
@@ -1183,13 +1183,13 @@
1183
1183
  "license": "MIT"
1184
1184
  },
1185
1185
  "node_modules/@ours.network/mcp": {
1186
- "version": "1.2.0-nightly.3",
1187
- "resolved": "https://registry.npmjs.org/@ours.network/mcp/-/mcp-1.2.0-nightly.3.tgz",
1188
- "integrity": "sha512-z0N5jn1jj9iwGRqU1d3AM2RX9mCr4T/WhASEKr+Vh52yNDMzhVjFlx0U0v8aS+GTfOQaPZTfL3TGMc17ETjh3g==",
1186
+ "version": "1.2.0-nightly.4",
1187
+ "resolved": "https://registry.npmjs.org/@ours.network/mcp/-/mcp-1.2.0-nightly.4.tgz",
1188
+ "integrity": "sha512-NcVDRmwOHx5zAwcIs6cMstSPyIEhl9f0mQNNsSbXYi+oGkamzdEPH02RHWrmoYCgvqiWjO8bhCqB43Mum8ozYg==",
1189
1189
  "license": "FSL-1.1-Apache-2.0",
1190
1190
  "dependencies": {
1191
- "@ours.network/cli": "2.8.1-nightly.1",
1192
- "@ours.network/sdk": "3.8.1-nightly.3"
1191
+ "@ours.network/cli": "2.8.1-nightly.2",
1192
+ "@ours.network/sdk": "3.8.1-nightly.4"
1193
1193
  },
1194
1194
  "bin": {
1195
1195
  "ours-mcp": "dist/cli.js"
@@ -1199,14 +1199,14 @@
1199
1199
  }
1200
1200
  },
1201
1201
  "node_modules/@ours.network/messenger-server": {
1202
- "version": "1.0.31-nightly.1",
1203
- "resolved": "https://registry.npmjs.org/@ours.network/messenger-server/-/messenger-server-1.0.31-nightly.1.tgz",
1204
- "integrity": "sha512-XB6N+Ppd4txmI0+2ZVFMWJxXi4DtMIDlFSHOzszNM5hopFebpw1Df+KKCK4Cgd/LZUkoOOYWGEYpib9m3RbHWw==",
1202
+ "version": "1.0.31-nightly.2",
1203
+ "resolved": "https://registry.npmjs.org/@ours.network/messenger-server/-/messenger-server-1.0.31-nightly.2.tgz",
1204
+ "integrity": "sha512-yWBFSzUwoKHfn7z//uesWmEzGOYFfgFsjBr7KXhGGAeXRvGbbbt1OHOg+ZPNhastXipxOrg+oeY94pQwJClxkA==",
1205
1205
  "license": "FSL-1.1-Apache-2.0",
1206
1206
  "dependencies": {
1207
1207
  "@eslint-community/regexpp": "4.12.2",
1208
1208
  "@fontsource-variable/jetbrains-mono": "^5.3.0",
1209
- "@ours.network/sdk": "3.8.1-nightly.3",
1209
+ "@ours.network/sdk": "3.8.1-nightly.4",
1210
1210
  "@radix-ui/react-dialog": "^1.1.23",
1211
1211
  "framer-motion": "^12.43.0",
1212
1212
  "jsqr": "^1.4.0",
@@ -1232,9 +1232,9 @@
1232
1232
  }
1233
1233
  },
1234
1234
  "node_modules/@ours.network/sdk": {
1235
- "version": "3.8.1-nightly.3",
1236
- "resolved": "https://registry.npmjs.org/@ours.network/sdk/-/sdk-3.8.1-nightly.3.tgz",
1237
- "integrity": "sha512-iUVCs+jryoeR2Ip/uHzpkJwbpoJeLDASUaE3lC+eYDj8dbI1MBwTpeOXFemUK052O6lA4ZWrhyRgE9pJU3a+Kw==",
1235
+ "version": "3.8.1-nightly.4",
1236
+ "resolved": "https://registry.npmjs.org/@ours.network/sdk/-/sdk-3.8.1-nightly.4.tgz",
1237
+ "integrity": "sha512-h0euEcW9VDEhSHV5ubB1wC6KCXV89EqNCVf05F2kUx4SSvEwPNLPHtN9i21H6qlBEq3RHrHnsl1/uo1l51LCMA==",
1238
1238
  "license": "FSL-1.1-Apache-2.0",
1239
1239
  "workspaces": [
1240
1240
  ".",
@@ -1254,12 +1254,12 @@
1254
1254
  }
1255
1255
  },
1256
1256
  "node_modules/@ours.network/tg-connector": {
1257
- "version": "1.0.1-nightly.1",
1258
- "resolved": "https://registry.npmjs.org/@ours.network/tg-connector/-/tg-connector-1.0.1-nightly.1.tgz",
1259
- "integrity": "sha512-/Do7EfINknDks3yYndOi9aLc1alxB50qk+WKv8DRuZpDB89DDMQ+DoSGQerdvsMp32nQ3LICL8TPjcdFMZfx8Q==",
1257
+ "version": "1.0.1-nightly.2",
1258
+ "resolved": "https://registry.npmjs.org/@ours.network/tg-connector/-/tg-connector-1.0.1-nightly.2.tgz",
1259
+ "integrity": "sha512-XTA2vXMLrXNq7PKVXHp8HluaMzTf2tx4kDy+EOu5sWwvfiP2f4qSCx+ESeebP//TNnSHfrNnGZZiCV0uVsO49g==",
1260
1260
  "license": "FSL-1.1-Apache-2.0",
1261
1261
  "dependencies": {
1262
- "@ours.network/sdk": "3.8.1-nightly.3",
1262
+ "@ours.network/sdk": "3.8.1-nightly.4",
1263
1263
  "undici": "^7.28.0"
1264
1264
  },
1265
1265
  "bin": {
@@ -1,43 +1,53 @@
1
1
  {
2
2
  "schema": 1,
3
3
  "channel": "nightly",
4
- "installerVersion": "1.2.1-nightly.7",
4
+ "installerVersion": "1.2.1-nightly.9",
5
5
  "packages": {
6
6
  "@ours.network/sdk": {
7
- "version": "3.8.1-nightly.3",
8
- "integrity": "sha512-iUVCs+jryoeR2Ip/uHzpkJwbpoJeLDASUaE3lC+eYDj8dbI1MBwTpeOXFemUK052O6lA4ZWrhyRgE9pJU3a+Kw=="
7
+ "version": "3.8.1-nightly.4",
8
+ "integrity": "sha512-h0euEcW9VDEhSHV5ubB1wC6KCXV89EqNCVf05F2kUx4SSvEwPNLPHtN9i21H6qlBEq3RHrHnsl1/uo1l51LCMA=="
9
9
  },
10
10
  "@ours.network/cli": {
11
- "version": "2.8.1-nightly.1",
12
- "integrity": "sha512-1Rmbqlr790FhqQW/wQ//YEL7b0Q2KVSCA2TlpjaLRkj9Xh55rOoazBJjFuW/fUpTz3Z1B3MUPK9LWiYd+A0kNA=="
11
+ "version": "2.8.1-nightly.2",
12
+ "integrity": "sha512-YirjiR6/Xs1p2LKOLVtao74xLXCsxgHz3gwbKyZ4Qtd9w6MF+kS/OEOLKozmIjWJ5FoqUxgvnnaudl5vhaeUkA=="
13
13
  },
14
14
  "@ours.network/tg-connector": {
15
- "version": "1.0.1-nightly.1",
16
- "integrity": "sha512-/Do7EfINknDks3yYndOi9aLc1alxB50qk+WKv8DRuZpDB89DDMQ+DoSGQerdvsMp32nQ3LICL8TPjcdFMZfx8Q=="
15
+ "version": "1.0.1-nightly.2",
16
+ "integrity": "sha512-XTA2vXMLrXNq7PKVXHp8HluaMzTf2tx4kDy+EOu5sWwvfiP2f4qSCx+ESeebP//TNnSHfrNnGZZiCV0uVsO49g=="
17
17
  },
18
18
  "@ours.network/cowork": {
19
- "version": "1.3.2-nightly.20260918.4d9242e",
20
- "integrity": "sha512-04D2lJ97SrTrkVRRbdTZ0y+KvQdG/bpnmFFjsyUqQTij+5zEkNizTG/xhcCL8m7Mb0XvVFL8WzJ62Xgh5ahlRw=="
19
+ "version": "1.3.2-nightly.20260919.5e6e5fd",
20
+ "integrity": "sha512-/3KEKXqriBsPiK8POyL/XgbP4ELsRs6quv0Q47cFCaesXAR4W44Q3NVs43brnj1AwMLroip0iXzqwlsJvpoaTw=="
21
21
  },
22
22
  "@ours.network/messenger-server": {
23
- "version": "1.0.31-nightly.1",
24
- "integrity": "sha512-XB6N+Ppd4txmI0+2ZVFMWJxXi4DtMIDlFSHOzszNM5hopFebpw1Df+KKCK4Cgd/LZUkoOOYWGEYpib9m3RbHWw=="
23
+ "version": "1.0.31-nightly.2",
24
+ "integrity": "sha512-yWBFSzUwoKHfn7z//uesWmEzGOYFfgFsjBr7KXhGGAeXRvGbbbt1OHOg+ZPNhastXipxOrg+oeY94pQwJClxkA=="
25
25
  },
26
26
  "@ours.network/fleet": {
27
- "version": "1.2.0-nightly.1",
28
- "integrity": "sha512-vioCobVbosmp9BgYo2XccjpPoVQd1wwV+Mt64Bm41/Vxtro2j85PUdy4AWcsY9jEFnJmj/UQfc5E5AxvlpPnrw=="
27
+ "version": "1.2.0-nightly.2",
28
+ "integrity": "sha512-vnTGjjl2ycd7FN4CKGblemFcq5/MWocTR0CQhGEbspYXRYiojGa3MRyLzJPOVJQbP0/zqIwSG5Vk9czMWHpjow=="
29
29
  },
30
30
  "@ours.network/mcp": {
31
- "version": "1.2.0-nightly.3",
32
- "integrity": "sha512-z0N5jn1jj9iwGRqU1d3AM2RX9mCr4T/WhASEKr+Vh52yNDMzhVjFlx0U0v8aS+GTfOQaPZTfL3TGMc17ETjh3g=="
31
+ "version": "1.2.0-nightly.4",
32
+ "integrity": "sha512-NcVDRmwOHx5zAwcIs6cMstSPyIEhl9f0mQNNsSbXYi+oGkamzdEPH02RHWrmoYCgvqiWjO8bhCqB43Mum8ozYg=="
33
33
  },
34
34
  "@ours.network/codex": {
35
- "version": "1.2.0-nightly.3",
36
- "integrity": "sha512-dIWDqbo6hJzujegHRO6x1SsBnh5p1c6gRGj/WkPha/hcN7s/27P4jqBnJ7czQyVGiNVFZ1aAxj4c9+CDLzHENw=="
35
+ "version": "1.2.0-nightly.4",
36
+ "integrity": "sha512-ZYzqWWcPv9snZH86AZxGfkT2lc9djIJuvBBWdDC7j7YJRjXVQfTVGNFtc5NoGesQYiiQFhiumSppMxfc3vHVDA=="
37
37
  },
38
38
  "@ours.network/claude-code": {
39
- "version": "1.2.0-nightly.3",
40
- "integrity": "sha512-7ta6KnvoHB2i/yoaykhXvzKFQTIOPtDHeTzG1PRwHjdTJ4vcGEh4jWYMd2c1jjj73lC3q9qW3LYSRutIJovtQg=="
39
+ "version": "1.2.0-nightly.4",
40
+ "integrity": "sha512-JNDhTMtxO2Xm0NNL/5Kgra/ovr62mP0YFcuvYJRRA+SH6+2Ick8eegeSVmzU97HPLBCTi8DwNqfkUxcCQUpwXA=="
41
+ }
42
+ },
43
+ "hostCli": {
44
+ "@ours.network/sdk": {
45
+ "version": "3.8.1-nightly.6",
46
+ "integrity": "sha512-kNCR1/0X5sTKCDe1oaEvxXHBkRa0B4N3mosbYZAzY8mu0sTY+TKx2u0ltQ4PRZ50ZuwzhxNe7BVbphu9b/87xw=="
47
+ },
48
+ "@ours.network/cli": {
49
+ "version": "2.8.1-nightly.4",
50
+ "integrity": "sha512-Lw9DVmY4vpAAm+ps8tD/eqJr4jwGSxGfpOtkvHvgjLzY442zM1od8/ZNyCV8d4YVxGOxuT72Fss9qgXnVR6bvQ=="
41
51
  }
42
52
  }
43
53
  }
@@ -20,10 +20,19 @@ export function releaseBinding(policy) {
20
20
  if (release?.schema !== 1 || !['stable', 'nightly'].includes(release.channel)) fail('invalid release binding');
21
21
  const pattern = release.channel === 'nightly' ? nightly : stable;
22
22
  if (typeof release.installerVersion !== 'string' || !pattern.test(release.installerVersion)) fail('invalid installer version/channel');
23
- if (!release.packages || JSON.stringify(Object.keys(release.packages).sort()) !== JSON.stringify([...names].sort())) fail('release must select exactly nine ours packages');
23
+ if (release.scope !== undefined && release.scope !== 'host-cli') fail('invalid release scope');
24
+ const required = release.scope === 'host-cli' ? ['@ours.network/cli', '@ours.network/sdk'] : names;
25
+ if (!release.packages || JSON.stringify(Object.keys(release.packages).sort()) !== JSON.stringify([...required].sort())) fail('release must select exactly the required ours packages');
26
+ if (release.scope === 'host-cli' && Object.hasOwn(release, 'hostCli')) fail('nested host CLI selection');
24
27
  for (const [name, entry] of Object.entries(release.packages)) {
25
28
  if (typeof entry?.version !== 'string' || !pattern.test(entry.version) || !/^sha512-[A-Za-z0-9+/]{86}==$/.test(entry.integrity ?? '')) fail(`invalid release artifact: ${name}`);
26
29
  }
30
+ if (Object.hasOwn(release, 'hostCli')) {
31
+ if (!release.hostCli || Object.keys(release.hostCli).sort().join(',') !== '@ours.network/cli,@ours.network/sdk') fail('host CLI must select exactly CLI and SDK');
32
+ for (const [name, entry] of Object.entries(release.hostCli)) {
33
+ if (typeof entry?.version !== 'string' || !pattern.test(entry.version) || !/^sha512-[A-Za-z0-9+/]{86}==$/.test(entry.integrity ?? '')) fail(`invalid host CLI artifact: ${name}`);
34
+ }
35
+ }
27
36
  if (!policy.packages || !Object.keys(policy.packages).length) fail('missing release source selection');
28
37
  for (const [name, entry] of Object.entries(policy.packages)) {
29
38
  if (entry?.type !== 'npm' || entry.version !== release.packages[name]?.version || Object.keys(entry).sort().join(',') !== 'type,version') fail(`source selection differs from release: ${name}`);
@@ -92,3 +101,11 @@ export function verifyRuntimeRelease(root) {
92
101
  if (!existsSync(path)) return { verified: false, reason: 'legacy runtime without sources' };
93
102
  return verifyReleaseGraph(root, json(path));
94
103
  }
104
+
105
+ /** The client command has its own exact dependency selection, never a graph bypass. */
106
+ export function hostCliPolicy(policy) {
107
+ const release = releaseBinding(policy);
108
+ if (!release?.hostCli) return null;
109
+ const { hostCli, ...base } = release;
110
+ return { release: { ...base, scope: 'host-cli', packages: hostCli }, packages: Object.fromEntries(Object.entries(hostCli).map(([name, p]) => [name, { type: 'npm', version: p.version }])) };
111
+ }
@@ -2,82 +2,92 @@
2
2
  "release": {
3
3
  "schema": 1,
4
4
  "channel": "nightly",
5
- "installerVersion": "1.2.1-nightly.7",
5
+ "installerVersion": "1.2.1-nightly.9",
6
6
  "packages": {
7
7
  "@ours.network/sdk": {
8
- "version": "3.8.1-nightly.3",
9
- "integrity": "sha512-iUVCs+jryoeR2Ip/uHzpkJwbpoJeLDASUaE3lC+eYDj8dbI1MBwTpeOXFemUK052O6lA4ZWrhyRgE9pJU3a+Kw=="
8
+ "version": "3.8.1-nightly.4",
9
+ "integrity": "sha512-h0euEcW9VDEhSHV5ubB1wC6KCXV89EqNCVf05F2kUx4SSvEwPNLPHtN9i21H6qlBEq3RHrHnsl1/uo1l51LCMA=="
10
10
  },
11
11
  "@ours.network/cli": {
12
- "version": "2.8.1-nightly.1",
13
- "integrity": "sha512-1Rmbqlr790FhqQW/wQ//YEL7b0Q2KVSCA2TlpjaLRkj9Xh55rOoazBJjFuW/fUpTz3Z1B3MUPK9LWiYd+A0kNA=="
12
+ "version": "2.8.1-nightly.2",
13
+ "integrity": "sha512-YirjiR6/Xs1p2LKOLVtao74xLXCsxgHz3gwbKyZ4Qtd9w6MF+kS/OEOLKozmIjWJ5FoqUxgvnnaudl5vhaeUkA=="
14
14
  },
15
15
  "@ours.network/tg-connector": {
16
- "version": "1.0.1-nightly.1",
17
- "integrity": "sha512-/Do7EfINknDks3yYndOi9aLc1alxB50qk+WKv8DRuZpDB89DDMQ+DoSGQerdvsMp32nQ3LICL8TPjcdFMZfx8Q=="
16
+ "version": "1.0.1-nightly.2",
17
+ "integrity": "sha512-XTA2vXMLrXNq7PKVXHp8HluaMzTf2tx4kDy+EOu5sWwvfiP2f4qSCx+ESeebP//TNnSHfrNnGZZiCV0uVsO49g=="
18
18
  },
19
19
  "@ours.network/cowork": {
20
- "version": "1.3.2-nightly.20260918.4d9242e",
21
- "integrity": "sha512-04D2lJ97SrTrkVRRbdTZ0y+KvQdG/bpnmFFjsyUqQTij+5zEkNizTG/xhcCL8m7Mb0XvVFL8WzJ62Xgh5ahlRw=="
20
+ "version": "1.3.2-nightly.20260919.5e6e5fd",
21
+ "integrity": "sha512-/3KEKXqriBsPiK8POyL/XgbP4ELsRs6quv0Q47cFCaesXAR4W44Q3NVs43brnj1AwMLroip0iXzqwlsJvpoaTw=="
22
22
  },
23
23
  "@ours.network/messenger-server": {
24
- "version": "1.0.31-nightly.1",
25
- "integrity": "sha512-XB6N+Ppd4txmI0+2ZVFMWJxXi4DtMIDlFSHOzszNM5hopFebpw1Df+KKCK4Cgd/LZUkoOOYWGEYpib9m3RbHWw=="
24
+ "version": "1.0.31-nightly.2",
25
+ "integrity": "sha512-yWBFSzUwoKHfn7z//uesWmEzGOYFfgFsjBr7KXhGGAeXRvGbbbt1OHOg+ZPNhastXipxOrg+oeY94pQwJClxkA=="
26
26
  },
27
27
  "@ours.network/fleet": {
28
- "version": "1.2.0-nightly.1",
29
- "integrity": "sha512-vioCobVbosmp9BgYo2XccjpPoVQd1wwV+Mt64Bm41/Vxtro2j85PUdy4AWcsY9jEFnJmj/UQfc5E5AxvlpPnrw=="
28
+ "version": "1.2.0-nightly.2",
29
+ "integrity": "sha512-vnTGjjl2ycd7FN4CKGblemFcq5/MWocTR0CQhGEbspYXRYiojGa3MRyLzJPOVJQbP0/zqIwSG5Vk9czMWHpjow=="
30
30
  },
31
31
  "@ours.network/mcp": {
32
- "version": "1.2.0-nightly.3",
33
- "integrity": "sha512-z0N5jn1jj9iwGRqU1d3AM2RX9mCr4T/WhASEKr+Vh52yNDMzhVjFlx0U0v8aS+GTfOQaPZTfL3TGMc17ETjh3g=="
32
+ "version": "1.2.0-nightly.4",
33
+ "integrity": "sha512-NcVDRmwOHx5zAwcIs6cMstSPyIEhl9f0mQNNsSbXYi+oGkamzdEPH02RHWrmoYCgvqiWjO8bhCqB43Mum8ozYg=="
34
34
  },
35
35
  "@ours.network/codex": {
36
- "version": "1.2.0-nightly.3",
37
- "integrity": "sha512-dIWDqbo6hJzujegHRO6x1SsBnh5p1c6gRGj/WkPha/hcN7s/27P4jqBnJ7czQyVGiNVFZ1aAxj4c9+CDLzHENw=="
36
+ "version": "1.2.0-nightly.4",
37
+ "integrity": "sha512-ZYzqWWcPv9snZH86AZxGfkT2lc9djIJuvBBWdDC7j7YJRjXVQfTVGNFtc5NoGesQYiiQFhiumSppMxfc3vHVDA=="
38
38
  },
39
39
  "@ours.network/claude-code": {
40
- "version": "1.2.0-nightly.3",
41
- "integrity": "sha512-7ta6KnvoHB2i/yoaykhXvzKFQTIOPtDHeTzG1PRwHjdTJ4vcGEh4jWYMd2c1jjj73lC3q9qW3LYSRutIJovtQg=="
40
+ "version": "1.2.0-nightly.4",
41
+ "integrity": "sha512-JNDhTMtxO2Xm0NNL/5Kgra/ovr62mP0YFcuvYJRRA+SH6+2Ick8eegeSVmzU97HPLBCTi8DwNqfkUxcCQUpwXA=="
42
+ }
43
+ },
44
+ "hostCli": {
45
+ "@ours.network/sdk": {
46
+ "version": "3.8.1-nightly.6",
47
+ "integrity": "sha512-kNCR1/0X5sTKCDe1oaEvxXHBkRa0B4N3mosbYZAzY8mu0sTY+TKx2u0ltQ4PRZ50ZuwzhxNe7BVbphu9b/87xw=="
48
+ },
49
+ "@ours.network/cli": {
50
+ "version": "2.8.1-nightly.4",
51
+ "integrity": "sha512-Lw9DVmY4vpAAm+ps8tD/eqJr4jwGSxGfpOtkvHvgjLzY442zM1od8/ZNyCV8d4YVxGOxuT72Fss9qgXnVR6bvQ=="
42
52
  }
43
53
  }
44
54
  },
45
55
  "packages": {
46
56
  "@ours.network/sdk": {
47
57
  "type": "npm",
48
- "version": "3.8.1-nightly.3"
58
+ "version": "3.8.1-nightly.4"
49
59
  },
50
60
  "@ours.network/cli": {
51
61
  "type": "npm",
52
- "version": "2.8.1-nightly.1"
62
+ "version": "2.8.1-nightly.2"
53
63
  },
54
64
  "@ours.network/tg-connector": {
55
65
  "type": "npm",
56
- "version": "1.0.1-nightly.1"
66
+ "version": "1.0.1-nightly.2"
57
67
  },
58
68
  "@ours.network/cowork": {
59
69
  "type": "npm",
60
- "version": "1.3.2-nightly.20260918.4d9242e"
70
+ "version": "1.3.2-nightly.20260919.5e6e5fd"
61
71
  },
62
72
  "@ours.network/messenger-server": {
63
73
  "type": "npm",
64
- "version": "1.0.31-nightly.1"
74
+ "version": "1.0.31-nightly.2"
65
75
  },
66
76
  "@ours.network/fleet": {
67
77
  "type": "npm",
68
- "version": "1.2.0-nightly.1"
78
+ "version": "1.2.0-nightly.2"
69
79
  },
70
80
  "@ours.network/mcp": {
71
81
  "type": "npm",
72
- "version": "1.2.0-nightly.3"
82
+ "version": "1.2.0-nightly.4"
73
83
  },
74
84
  "@ours.network/codex": {
75
85
  "type": "npm",
76
- "version": "1.2.0-nightly.3"
86
+ "version": "1.2.0-nightly.4"
77
87
  },
78
88
  "@ours.network/claude-code": {
79
89
  "type": "npm",
80
- "version": "1.2.0-nightly.3"
90
+ "version": "1.2.0-nightly.4"
81
91
  }
82
92
  }
83
93
  }
@@ -0,0 +1,117 @@
1
+ import { lstatSync, readFileSync, writeFileSync, realpathSync, unlinkSync, chmodSync, symlinkSync, renameSync, mkdirSync } from 'node:fs';
2
+ import { join, resolve, isAbsolute, dirname } from 'node:path';
3
+ import { createHash, randomUUID } from 'node:crypto';
4
+ import { buildManagedCli } from './managed-cli.mjs';
5
+
6
+ import { createRequire } from 'node:module';
7
+ import { releaseBinding, verifyReleaseGraph } from '../assets/scripts/maintenance/release-graph.mjs';
8
+
9
+ const marker = '// ours-managed-cli-v1 ';
10
+ const hash = bytes => createHash('sha256').update(bytes).digest('hex');
11
+ function stat(path) {
12
+ try { return lstatSync(path); } catch (error) { if (error.code === 'ENOENT') return null; throw error; }
13
+ }
14
+ function privateAcquisition(home, root) {
15
+ for (const path of [join(home, '.ours-client-install'), root]) {
16
+ const value = lstatSync(path);
17
+ if (!value.isDirectory() || value.uid !== process.getuid() || (value.mode & 0o077) || realpathSync(path) !== path) throw new Error('Unsafe private CLI directory');
18
+ }
19
+ }
20
+ /** Publish the native client, retaining a verified migration launcher for rollback. */
21
+ export async function publishClientCli(effects, packagePath, { policy = {}, isolated = false } = {}) {
22
+ const prefix = (await effects.run('npm', ['prefix', '--global'])).stdout.trim();
23
+ if (!isAbsolute(prefix) || resolve(prefix) !== prefix) throw new Error('npm global prefix must be an absolute normalized path');
24
+ const entry = join(prefix, 'bin', 'ours');
25
+ const before = stat(entry);
26
+ let previous;
27
+ if (before) {
28
+ if (before.uid !== process.getuid()) throw new Error('Refusing to replace a CLI owned by another user');
29
+ if (before.isSymbolicLink()) {
30
+ const resolved = realpathSync(entry);
31
+ const privateBase = join(effects.home, '.ours-client-install') + '/';
32
+ if (resolved.startsWith(privateBase)) {
33
+ const packageRoot = dirname(dirname(resolved));
34
+ const acquisitionRoot = dirname(dirname(dirname(packageRoot)));
35
+ privateAcquisition(effects.home, acquisitionRoot);
36
+ const retained = JSON.parse(readFileSync(join(acquisitionRoot, 'sources.json')));
37
+ if (releaseBinding(retained)?.scope !== 'host-cli') throw new Error('Unrecognized private CLI entry');
38
+ verifyReleaseGraph(acquisitionRoot, retained);
39
+ const pkg = JSON.parse(readFileSync(join(packageRoot, 'package.json')));
40
+ if (pkg.name !== '@ours.network/cli' || resolve(packageRoot, pkg.bin?.ours ?? '') !== resolved) throw new Error('Unrecognized private CLI target');
41
+ } else {
42
+ const npmRoot = (await effects.run('npm', ['root', '--global'])).stdout.trim();
43
+ if (!isAbsolute(npmRoot)) throw new Error('Invalid npm global package root');
44
+ const root = realpathSync(join(npmRoot, '@ours.network/cli'));
45
+ const manifest = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
46
+ if (manifest.name !== '@ours.network/cli' || typeof manifest.bin?.ours !== 'string' || resolved !== realpathSync(resolve(root, manifest.bin.ours))) throw new Error('Refusing to replace an unrelated ours symlink');
47
+ }
48
+ } else {
49
+ if (!before.isFile() || before.nlink !== 1 || (before.mode & 0o022)) throw new Error('Refusing to replace an unsafe CLI entry');
50
+ previous = readFileSync(entry);
51
+ const line = previous.toString().split('\n')[1];
52
+ if (!line?.startsWith(marker)) throw new Error('Refusing to replace an unknown ours executable');
53
+ const binding = JSON.parse(line.slice(marker.length));
54
+ if (binding.schema !== 1 || previous.toString() !== buildManagedCli(binding.recordPath, binding.installerPath)) throw new Error('Managed CLI launcher was modified; no replacement performed');
55
+ const backup = join(effects.home, '.ours-client', `previous-cli-${hash(previous)}.cjs`);
56
+ const backupDir = lstatSync(join(effects.home, '.ours-client'));
57
+ if (!backupDir.isDirectory() || backupDir.uid !== process.getuid() || (backupDir.mode & 0o7777) !== 0o700) throw new Error('Managed CLI backup requires a private owned directory');
58
+ const saved = stat(backup);
59
+ if (!saved) writeFileSync(backup, previous, { mode: 0o600, flag: 'wx' });
60
+ else if (!saved.isFile() || saved.nlink !== 1 || saved.uid !== process.getuid() || (saved.mode & 0o7777) !== 0o600 || saved.size !== previous.length || !readFileSync(backup).equals(previous)) throw new Error('Managed CLI backup is unsafe or differs');
61
+ const current = lstatSync(entry);
62
+ if (current.dev !== before.dev || current.ino !== before.ino || !readFileSync(entry).equals(previous)) throw new Error('CLI changed during publication');
63
+ if (!isolated) unlinkSync(entry);
64
+ effects.out(`Previous managed CLI saved at ${backup}; server maintenance remains available through ours-install server.`);
65
+ }
66
+ }
67
+ if (isolated) {
68
+ const root = dirname(dirname(dirname(packagePath)));
69
+ if (!realpathSync(root).startsWith(join(effects.home, '.ours-client-install') + '/') || releaseBinding(policy)?.scope !== 'host-cli') throw new Error('Invalid private CLI acquisition');
70
+ privateAcquisition(effects.home, root);
71
+ verifyReleaseGraph(root, policy);
72
+ const selected = JSON.parse(readFileSync(join(packagePath, 'package.json')));
73
+ const target = realpathSync(resolve(packagePath, selected.bin.ours));
74
+ if (!target.startsWith(realpathSync(packagePath) + '/')) throw new Error('CLI entry escapes selected package');
75
+ const sdkEntry = realpathSync(createRequire(target).resolve('@ours.network/sdk'));
76
+ if (!sdkEntry.startsWith(root + '/')) throw new Error('Private CLI SDK escaped verified acquisition');
77
+ const sdk = JSON.parse(readFileSync(join(dirname(dirname(sdkEntry)), 'package.json')));
78
+ if (sdk.name !== '@ours.network/sdk' || sdk.version !== policy.release.packages['@ours.network/sdk'].version) throw new Error('Private CLI SDK differs from selection');
79
+ mkdirSync(dirname(entry), { recursive: true });
80
+ const binDir = lstatSync(dirname(entry));
81
+ if (!binDir.isDirectory() || binDir.uid !== process.getuid() || (binDir.mode & 0o022)) throw new Error('CLI bin directory must be owned and not writable by others');
82
+ const current = stat(entry);
83
+ if (before ? !current || current.dev !== before.dev || current.ino !== before.ino : current) throw new Error('CLI entry changed during publication');
84
+ const staged = entry + '.' + randomUUID();
85
+ try {
86
+ symlinkSync(target, staged);
87
+ renameSync(staged, entry);
88
+ } catch (error) {
89
+ if (stat(staged)) unlinkSync(staged);
90
+ if (previous && !stat(entry)) writeFileSync(entry, previous, { mode: before.mode & 0o777, flag: 'wx' });
91
+ throw error;
92
+ }
93
+ if (realpathSync(entry) !== target) throw new Error('Private CLI entry changed during publication');
94
+ verifyReleaseGraph(root, policy);
95
+ return entry;
96
+ }
97
+ try {
98
+ await effects.run('npm', ['install', '--global', '--install-links=false', '--offline', '--ignore-scripts', '--no-audit', '--no-fund', packagePath]);
99
+ } catch (error) {
100
+ if (previous && !stat(entry)) {
101
+ writeFileSync(entry, previous, { mode: before.mode & 0o777, flag: 'wx' });
102
+ chmodSync(entry, before.mode & 0o777);
103
+ }
104
+ throw error;
105
+ }
106
+ const published = stat(entry);
107
+ if (!published?.isSymbolicLink() || published.uid !== process.getuid()) throw new Error('CLI publication did not create the expected owned npm entry');
108
+ const npmRoot = (await effects.run('npm', ['root', '--global'])).stdout.trim();
109
+ if (!isAbsolute(npmRoot) || resolve(npmRoot) !== npmRoot) throw new Error('Invalid npm global package root');
110
+ const root = realpathSync(join(npmRoot, '@ours.network/cli'));
111
+ const manifest = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
112
+ const selected = JSON.parse(readFileSync(join(packagePath, 'package.json'), 'utf8'));
113
+ const target = typeof manifest.bin?.ours === 'string' ? resolve(root, manifest.bin.ours) : '';
114
+ if (manifest.name !== '@ours.network/cli' || manifest.version !== selected.version || !target.startsWith(root + '/') || realpathSync(entry) !== realpathSync(target) || !stat(target)?.isFile()) throw new Error('Published CLI does not match the selected package');
115
+ if (!readFileSync(target).equals(readFileSync(resolve(packagePath, selected.bin.ours)))) throw new Error('Published CLI entry differs from the selected artifact');
116
+ return entry;
117
+ }
package/lib/effects.mjs CHANGED
@@ -11,6 +11,7 @@
11
11
  // state-directory guard and the enable/reload. The installer never touches a
12
12
  // unit file or the service manager directly.
13
13
 
14
+ import { publishClientCli } from './client-cli.mjs';
14
15
  import { spawnSync, execFileSync } from 'node:child_process';
15
16
  import { chmodSync, closeSync, constants, cpSync, existsSync, fstatSync, lstatSync, openSync, readFileSync, mkdirSync, mkdtempSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
16
17
  import { homedir, userInfo, platform as osPlatform, release as osRelease, arch as osArch } from 'node:os';
@@ -26,7 +27,7 @@ import { classifyHarnessProbe } from './logic.mjs';
26
27
  import { qualifyDockerRuntime, refreshDockerPolicyCopy } from './docker-runtime-repair.mjs';
27
28
  import { classifyStateDir } from './detect.mjs';
28
29
  import { BASE_RECORDS, CONTEXT, readBuildRecords, equalBuildRecords, initializeBuildMarker } from '../assets/scripts/maintenance/build-context.mjs';
29
- import { releaseBinding, verifyReleaseGraph, verifyRuntimeRelease } from '../assets/scripts/maintenance/release-graph.mjs';
30
+ import { hostCliPolicy, releaseBinding, verifyReleaseGraph, verifyRuntimeRelease } from '../assets/scripts/maintenance/release-graph.mjs';
30
31
 
31
32
  /** GET http://127.0.0.1:<port>/state-dir — the unauthenticated identity probe. */
32
33
  async function probePort(port, { timeoutMs = 1500 } = {}) {
@@ -1155,8 +1156,8 @@ export function networkEffects(effects) {
1155
1156
  },
1156
1157
  async discoverClientProfile(endpoint, credentialPath) {
1157
1158
  const url = new URL(endpoint);
1158
- if (url.protocol !== 'http:' || url.username || url.password || url.pathname !== '/' || url.search || url.hash)
1159
- throw new Error('Client endpoint must be an HTTP origin');
1159
+ if ((url.protocol !== 'http:' && url.protocol !== 'https:') || url.username || url.password || url.pathname !== '/' || url.search || url.hash)
1160
+ throw new Error('Client endpoint must be an HTTP or HTTPS origin');
1160
1161
  const response = await fetch(`${url.origin}/selection`, { redirect: 'error', signal: AbortSignal.timeout(5000) });
1161
1162
  if (!response.ok) throw new Error(`Daemon selection answered HTTP ${response.status}`);
1162
1163
  const selection = await response.json();
@@ -1195,18 +1196,20 @@ export function networkEffects(effects) {
1195
1196
  atomicWriteConfig(configPath, JSON.stringify(saved, null, 2) + '\n');
1196
1197
  return { configPath, profile: validateHostProfile(saved), settings };
1197
1198
  },
1198
- async acquireClientPackages(configPath, sourcesPath, integrations, { refresh = false } = {}) {
1199
+ async acquireClientPackages(configPath, sourcesPath, integrations, { refresh = false, hostCliOnly = false } = {}) {
1199
1200
  const manifest = JSON.parse(readFileSync(sourcesPath, 'utf8'));
1200
- // Public SDK client APIs are actual integration dependencies; Fleet also owns CLI usage.
1201
- const selected = [...new Set(['sdk', ...(integrations.includes('fleet') ? ['cli'] : []), ...integrations])];
1201
+ // Every client installation includes the native CLI and its SDK dependency.
1202
+ const selected = [...new Set(['sdk', 'cli', ...integrations])];
1202
1203
  const packages = selectSourcePackages(manifest, 'client', selected);
1203
- const selectionKey = refresh ? JSON.stringify([configPath, manifest, integrations]) : configPath;
1204
+ const selectionKey = JSON.stringify([hostCliOnly ? 'host-cli-private-v1' : 'host-cli-v1', configPath, ...(hostCliOnly ? [manifest] : []), ...(refresh ? [manifest, integrations] : [])]);
1204
1205
  const root = join(home, '.ours-client-install', createHash('sha256').update(selectionKey).digest('hex').slice(0, 16));
1205
1206
  const hasGit = Object.values(packages).some(selection => selection.source);
1206
1207
  await effects.run('npm', ['--version']);
1207
1208
  if (hasGit) {
1208
1209
  for (const command of ['python3', 'git', 'make', 'cc']) await effects.run(command, ['--version']);
1209
1210
  }
1211
+ // This directory retains active executables, not disposable download cache.
1212
+ ensurePrivateDirectory(join(home, '.ours-client-install'));
1210
1213
  ensurePrivateDirectory(root);
1211
1214
  const retained = join(root, 'sources.json');
1212
1215
  const bytes = readFileSync(sourcesPath);
@@ -1231,13 +1234,26 @@ export function networkEffects(effects) {
1231
1234
  writePrivateNew(join(root, '.packages-ready'), 'ready\n');
1232
1235
  }
1233
1236
  verifyReleaseGraph(root, manifest, { requiredPackages: Object.keys(packages) });
1237
+ const cliPolicy = hostCliOnly ? null : hostCliPolicy(manifest);
1234
1238
  // Local acquisition alone does not publish native commands. Use the user's
1235
1239
  // configured npm prefix and retained dependency closure, including on retry.
1236
1240
  for (const name of integrations.filter(name => name === 'fleet' || name === 'codex')) {
1237
1241
  await effects.run('npm', ['install', '--global', '--install-links=false', '--offline', '--ignore-scripts', '--no-audit', '--no-fund', join(root, 'node_modules', '@ours.network', name)]);
1238
1242
  }
1243
+ let cliBin = join(root, 'node_modules/.bin/ours');
1244
+ if (cliPolicy) {
1245
+ const cliSources = join(root, 'host-cli-sources.json');
1246
+ const cliBytes = JSON.stringify(cliPolicy, null, 2) + '\n';
1247
+ if (!existsSync(cliSources)) writePrivateNew(cliSources, cliBytes);
1248
+ else if (readFileSync(cliSources, 'utf8') !== cliBytes) throw new Error('Retained host CLI source selection differs');
1249
+ const acquired = await effects.acquireClientPackages(configPath, cliSources, [], { refresh, hostCliOnly: true });
1250
+ cliBin = acquired.cliBin;
1251
+ } else {
1252
+ // Publish last so optional integration installation cannot downgrade ours.
1253
+ await publishClientCli(effects, join(root, 'node_modules', '@ours.network/cli'), { policy: manifest, isolated: hostCliOnly });
1254
+ }
1239
1255
  const localPackages = Object.fromEntries(integrations.filter(n => n !== 'fleet').map(name => [name, join(root, 'node_modules', '@ours.network', name)]));
1240
- return { localPackages, packages: {}, fleetBin: integrations.includes('fleet') ? join(root, 'node_modules/.bin/ours-fleet') : null };
1256
+ return { localPackages, packages: {}, cliBin, fleetBin: integrations.includes('fleet') ? join(root, 'node_modules/.bin/ours-fleet') : null };
1241
1257
  },
1242
1258
  async prepareClientMarketplace(name, packagePath) {
1243
1259
  const acquisitionRoot = dirname(dirname(dirname(packagePath)));
@@ -1246,7 +1262,7 @@ export function networkEffects(effects) {
1246
1262
  const release = releaseBinding(policy);
1247
1263
  const integrationsPath = join(acquisitionRoot, 'integrations.json');
1248
1264
  const integrations = existsSync(integrationsPath) ? JSON.parse(readFileSync(integrationsPath, 'utf8')) : null;
1249
- const requiredPackages = integrations ? [...new Set(['sdk', ...(integrations.includes('fleet') ? ['cli'] : []), ...integrations])].map(name => '@ours.network/' + name) : Object.keys(policy.packages ?? {});
1265
+ const requiredPackages = integrations ? [...new Set(['sdk', 'cli', ...integrations])].map(name => '@ours.network/' + name) : Object.keys(policy.packages ?? {});
1250
1266
  verifyReleaseGraph(acquisitionRoot, policy, { requiredPackages });
1251
1267
  const root = join(acquisitionRoot, 'marketplaces', name);
1252
1268
  const plugin = join(root, 'plugins', 'ours');
@@ -101,7 +101,7 @@ function clientRetry(effects, imported, integrations) {
101
101
  effects.out(info('Saved profile and settings retained. Run ours-install, choose Connect to an existing server, and reuse the saved profile to finish the interactive Fleet configuration.'));
102
102
  return;
103
103
  }
104
- const args = ['client', 'install', '--config', imported.configPath, '--integrations', integrations.join(','), '--sources', imported.settings.sourcesPath];
104
+ const args = ['client', 'install', '--config', imported.configPath, '--integrations', integrations.join(',') || 'none', '--sources', imported.settings.sourcesPath];
105
105
  if (imported.settings.fleetSettingsPath) args.push('--fleet-settings', imported.settings.fleetSettingsPath);
106
106
  const command = `ours-install client install ${args.slice(2).map((value, i) => i % 2 === 0 ? value : shellQuote(value)).join(' ')}`;
107
107
  effects.out(info(`Saved profile and settings retained; re-run ${command}`));
@@ -1428,7 +1428,7 @@ export async function runClientCommand(command, effects) {
1428
1428
  let profile;
1429
1429
  if (configPath) profile = validateHostProfile(effects.readProfile(configPath));
1430
1430
  else if (!command.preset && effects.interactive) {
1431
- const endpoint = await effects.askLine('Server HTTP endpoint: ', 'http://127.0.0.1:3050');
1431
+ const endpoint = await effects.askLine('Server HTTP or HTTPS endpoint: ', 'http://127.0.0.1:3050');
1432
1432
  const credentialPath = await effects.askLine('Private issued-token file: ', '');
1433
1433
  if (!credentialPath) throw new InstallUsageError('Client setup requires an issued-token file');
1434
1434
  profile = await effects.discoverClientProfile(endpoint, credentialPath);
@@ -1449,13 +1449,13 @@ export async function runClientCommand(command, effects) {
1449
1449
  integrations = [];
1450
1450
  for (const name of ['codex', 'claude-code', 'fleet']) if (await effects.ask(`Install ${name}?`, name !== 'fleet')) integrations.push(name);
1451
1451
  }
1452
- if (!Array.isArray(integrations) || !integrations.length || integrations.some(name => !['codex', 'claude-code', 'fleet'].includes(name)) || new Set(integrations).size !== integrations.length) throw new InstallUsageError('installer.integrations must select codex, claude-code and/or fleet');
1452
+ if (!Array.isArray(integrations) || integrations.some(name => !['codex', 'claude-code', 'fleet'].includes(name)) || new Set(integrations).size !== integrations.length) throw new InstallUsageError('installer.integrations must be an array of codex, claude-code and/or fleet; use an empty array for CLI only');
1453
1453
  let fleetSettingsPath = command.preset ? command.fleetSettingsPath : settings?.fleetSettingsPath;
1454
1454
  if (command.nonInteractive && integrations.includes('fleet') && !fleetSettingsPath) throw new InstallUsageError('Fleet in CLI mode requires --fleet-settings; no interactive wizard will be opened');
1455
1455
  if (fleetSettingsPath !== undefined && (typeof fleetSettingsPath !== 'string' || !fleetSettingsPath))
1456
1456
  throw new InstallUsageError('installer.fleetSettingsPath must be a non-empty path when supplied');
1457
1457
  if (fleetSettingsPath) fleetSettingsPath = resolve(settingsBase, fleetSettingsPath);
1458
- const selectedClients = [...new Set(['sdk', ...(integrations.includes('fleet') ? ['cli'] : []), ...integrations])];
1458
+ const selectedClients = [...new Set(['sdk', 'cli', ...integrations])];
1459
1459
  let sourcesPath = settings?.sourcesPath;
1460
1460
  let resolvedSources;
1461
1461
  if (command.sourcePolicy) {
package/lib/setup.mjs CHANGED
@@ -11,7 +11,7 @@ import { validateIdentityName } from './server-onboarding.mjs';
11
11
  import { validateFleetSettings } from './fleet-settings.mjs';
12
12
 
13
13
  const maintenance = new Set(['status', 'start', 'stop', 'restart', 'rebuild', 'access-issue', 'access-replace', 'backup', 'restore', 'reset']);
14
- const clientPackages = integrations => [...new Set(['sdk', ...(integrations.includes('fleet') ? ['cli'] : []), ...integrations])];
14
+ const clientPackages = integrations => [...new Set(['sdk', 'cli', ...integrations])];
15
15
 
16
16
  export function completeReleasePolicy(retained, supplied) {
17
17
  if (retained?.release) {
@@ -32,7 +32,7 @@ export async function prepareSetupPlan(options, effects) {
32
32
  options = validateSetupOptions(options, { interactive: options.interactive });
33
33
  if (effects.platform?.platform === 'win32') throw new InstallUsageError('Run ours-install inside WSL with Docker Desktop integration on Windows. Direct Windows Node installations are not supported.');
34
34
  const plan = { ...options };
35
- if (options.scope !== 'server' && options.integrations.length) {
35
+ if (options.scope !== 'server') {
36
36
  for (const name of ['OURS_API_TOKEN', 'OURS_PORT', 'OURS_STATE_DIR', 'OURS_DAEMON_ID']) {
37
37
  if (effects.env?.[name]?.trim()) throw new InstallUsageError(`${name} conflicts with the selected client profile. Clear this override before full-stack/client setup; nothing was changed.`);
38
38
  }
@@ -61,13 +61,13 @@ export async function prepareSetupPlan(options, effects) {
61
61
  }
62
62
  if (options.operation === 'install' && !options.migrateFrom) {
63
63
  const retained = readObject(effects, plan.existing.sourcesPath, 'Retained source policy');
64
- plan.sourcePolicy = options.scope === 'server' || !options.integrations?.length ? retained : completeReleasePolicy(retained, options.sources ? policy : null);
64
+ plan.sourcePolicy = options.scope === 'server' ? retained : completeReleasePolicy(retained, options.sources ? policy : null);
65
65
  }
66
66
  } else if (options.operation === 'update') {
67
67
  throw new InstallUsageError('Update requires an existing installation.json; choose install for a new installation');
68
68
  }
69
69
  // Reject a local client already attached to another server before changing the server.
70
- const saved = options.scope === 'all' && options.integrations.length ? effects.readManagedClientProfile() : null;
70
+ const saved = options.scope === 'all' ? effects.readManagedClientProfile() : null;
71
71
  if (saved && (!plan.existing || saved.expectedInstanceId !== plan.existing.instanceId || saved.endpoint !== `http://127.0.0.1:${plan.port}`)) {
72
72
  throw new InstallUsageError('This user already has clients attached to a different server; their saved connection was not changed');
73
73
  }
@@ -82,12 +82,12 @@ export async function prepareSetupPlan(options, effects) {
82
82
  }
83
83
  // Dry-run never spawns a resolver or acquires an installation lock.
84
84
  if (!options.dryRun) {
85
- if (options.scope === 'all' && options.operation === 'update' && options.integrations.length) {
85
+ if (options.scope === 'all' && options.operation === 'update') {
86
86
  const running = await effects.serverLifecycle(plan.existing, 'status', ['daemon']);
87
87
  if (!running.includes('daemon')) throw new InstallUsageError('Full-stack update requires the selected daemon to be running for client verification. Start it first, or use server update to preserve its stopped state.');
88
88
  }
89
89
  if (options.scope !== 'client') await effects.resolveSourcePolicy(plan.sourcePolicy, 'server');
90
- if (options.scope !== 'server' && options.integrations.length) await effects.resolveSourcePolicy(plan.sourcePolicy, 'client', clientPackages(options.integrations));
90
+ if (options.scope !== 'server') await effects.resolveSourcePolicy(plan.sourcePolicy, 'client', clientPackages(options.integrations));
91
91
  }
92
92
  return plan;
93
93
  }
@@ -100,7 +100,7 @@ export async function executeSetupPlan(plan, effects, { server = runServerComman
100
100
  if (plan.dryRun) {
101
101
  effects.out(info('Preview only. No packages, identities, credentials or services will be changed.'));
102
102
  if (plan.scope !== 'client') effects.out(info('Server: prerequisites → runtime preparation/update → retained identity/state restoration → readiness.'));
103
- if (plan.scope !== 'server' && plan.integrations.length) effects.out(info('Clients: private connection → exact packages → selected integrations → Fleet settings when selected.'));
103
+ if (plan.scope !== 'server') effects.out(info('Clients: private connection → exact packages → selected integrations → Fleet settings when selected.'));
104
104
  return 0;
105
105
  }
106
106
  let clientConfig = plan.config;
@@ -118,14 +118,14 @@ export async function executeSetupPlan(plan, effects, { server = runServerComman
118
118
  effects.out(ok('Retained identities verified.'));
119
119
  } else effects.out(info('Daemon remains stopped; stored identities are retained and will restore on the next start.'));
120
120
  }
121
- if (plan.scope === 'all' && plan.integrations.length) {
121
+ if (plan.scope === 'all') {
122
122
  clientPolicy = completeReleasePolicy(readObject(effects, record.sourcesPath, 'Server source policy'), plan.sourcePolicy);
123
123
  effects.out(heading('Connect local clients'));
124
124
  const handoff = await effects.prepareLocalClient(record, plan.integrations, plan.fleetSettingsPath);
125
125
  clientConfig = handoff.configPath;
126
126
  }
127
127
  }
128
- if (plan.scope !== 'server' && plan.integrations.length) {
128
+ if (plan.scope !== 'server') {
129
129
  const result = await client({ role: 'client', operation: 'install', config: clientConfig,
130
130
  integrations: plan.integrations, fleetSettingsPath: plan.fleetSettingsPath, sourcePolicy: clientPolicy,
131
131
  preset: true, nonInteractive: !plan.interactive }, effects);
package/lib/target.mjs CHANGED
@@ -85,15 +85,15 @@ export function validateHostProfile(value) {
85
85
  const mixed = LEGACY_PROFILE_KEYS.filter((key) => Object.hasOwn(value, key));
86
86
  if (mixed.length) throw profileError(`legacy selection keys cannot be mixed with a host profile (${mixed.join(', ')}).`);
87
87
  const { endpoint, expectedInstanceId, credentialPath } = value;
88
- if (typeof endpoint !== 'string' || endpoint.trim() !== endpoint || endpoint === '') throw profileError('endpoint must be a non-empty HTTP origin.');
88
+ if (typeof endpoint !== 'string' || endpoint.trim() !== endpoint || endpoint === '') throw profileError('endpoint must be a non-empty HTTP or HTTPS origin.');
89
89
  if (typeof expectedInstanceId !== 'string' || !PROFILE_UUID.test(expectedInstanceId)) throw profileError('expectedInstanceId must be a lowercase UUID.');
90
90
  if (typeof credentialPath !== 'string' || credentialPath === '' || !credentialPath.startsWith('/') || resolve(credentialPath) !== credentialPath) {
91
91
  throw profileError('credentialPath must be a normalized absolute path.');
92
92
  }
93
93
  let url;
94
- try { url = new URL(endpoint); } catch { throw profileError('endpoint must be an HTTP origin.'); }
95
- if (url.protocol !== 'http:' || url.username || url.password || url.pathname !== '/' || url.search || url.hash) {
96
- throw profileError('endpoint must be an HTTP origin without credentials, path, query, or fragment.');
94
+ try { url = new URL(endpoint); } catch { throw profileError('endpoint must be an HTTP or HTTPS origin.'); }
95
+ if ((url.protocol !== 'http:' && url.protocol !== 'https:') || url.username || url.password || url.pathname !== '/' || url.search || url.hash) {
96
+ throw profileError('endpoint must be an HTTP or HTTPS origin without credentials, path, query, or fragment.');
97
97
  }
98
98
  return { endpoint: url.origin, expectedInstanceId, credentialPath };
99
99
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/install",
3
- "version": "1.2.1-nightly.7",
3
+ "version": "1.2.1-nightly.9",
4
4
  "private": false,
5
5
  "description": "The all-in-one ours.network installer: one shared daemon, MCP, cowork, Telegram, Fleet initialization, harness plugins, Human identity, progress UI, and guided next steps.",
6
6
  "type": "module",